@rubytech/create-maxy-code 0.1.57 → 0.1.59

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 (77) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +30 -19
  3. package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +35 -90
  4. package/payload/platform/plugins/docs/references/admin-session.md +17 -16
  5. package/payload/platform/plugins/docs/references/platform.md +3 -3
  6. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  7. package/payload/platform/plugins/preval/.claude-plugin/plugin.json +8 -0
  8. package/payload/platform/plugins/preval/PLUGIN.md +39 -0
  9. package/payload/platform/plugins/preval/skills/property-preval/SKILL.md +78 -0
  10. package/payload/platform/plugins/preval/skills/property-preval/references/render.py +265 -0
  11. package/payload/platform/plugins/preval/skills/property-preval/references/template-inputs.schema.json +143 -0
  12. package/payload/platform/plugins/preval/skills/property-preval/references/template.html +253 -0
  13. package/payload/platform/plugins/property-data/.claude-plugin/plugin.json +1 -1
  14. package/payload/platform/plugins/property-data/PLUGIN.md +46 -1
  15. package/payload/platform/plugins/property-data/mcp/dist/index.js +81 -1
  16. package/payload/platform/plugins/property-data/mcp/dist/index.js.map +1 -1
  17. package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.d.ts +10 -0
  18. package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.d.ts.map +1 -1
  19. package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.js +13 -0
  20. package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.js.map +1 -1
  21. package/payload/platform/plugins/property-data/mcp/src/__tests__/endpoints.test.ts +252 -0
  22. package/payload/platform/plugins/property-data/mcp/src/__tests__/schema-parity.test.ts +16 -2
  23. package/payload/platform/plugins/property-data/mcp/src/index.ts +155 -0
  24. package/payload/platform/plugins/property-data/mcp/src/tools/endpoints.ts +20 -0
  25. package/payload/platform/plugins/property-data/skills/property-data/SKILL.md +19 -0
  26. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  27. package/payload/platform/services/claude-session-manager/dist/http-server.js +25 -260
  28. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  29. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +3 -7
  30. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
  31. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +6 -32
  32. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
  33. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +6 -24
  34. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  35. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +18 -91
  36. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  37. package/payload/platform/services/claude-session-manager/dist/reaper.d.ts.map +1 -1
  38. package/payload/platform/services/claude-session-manager/dist/reaper.js +0 -11
  39. package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
  40. package/payload/platform/services/claude-session-manager/dist/session-store.d.ts +0 -21
  41. package/payload/platform/services/claude-session-manager/dist/session-store.d.ts.map +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/session-store.js.map +1 -1
  43. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  44. package/payload/platform/templates/agents/admin/SOUL.md +1 -1
  45. package/payload/premium-plugins/real-agent/BUNDLE.md +5 -3
  46. package/payload/premium-plugins/real-agent/agents/negotiator.md +1 -1
  47. package/payload/premium-plugins/real-agent/agents/valuer.md +1 -1
  48. package/payload/premium-plugins/real-agent/plugins/.claude-plugin/marketplace.json +5 -0
  49. package/payload/premium-plugins/real-agent/plugins/preval/.claude-plugin/plugin.json +8 -0
  50. package/payload/premium-plugins/real-agent/plugins/preval/PLUGIN.md +39 -0
  51. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/SKILL.md +78 -0
  52. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/render.py +265 -0
  53. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template-inputs.schema.json +143 -0
  54. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template.html +253 -0
  55. package/payload/premium-plugins/real-agent/plugins/property-data/.claude-plugin/plugin.json +1 -1
  56. package/payload/premium-plugins/real-agent/plugins/property-data/PLUGIN.md +46 -1
  57. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/index.js +81 -1
  58. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/index.js.map +1 -1
  59. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.d.ts +10 -0
  60. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.d.ts.map +1 -1
  61. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.js +13 -0
  62. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.js.map +1 -1
  63. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/__tests__/endpoints.test.ts +252 -0
  64. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/__tests__/schema-parity.test.ts +16 -2
  65. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/index.ts +155 -0
  66. package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/tools/endpoints.ts +20 -0
  67. package/payload/premium-plugins/real-agent/plugins/property-data/skills/property-data/SKILL.md +19 -0
  68. package/payload/server/public/assets/{admin-2ZJmzJ_g.js → admin-nqCBFiSl.js} +1 -1
  69. package/payload/server/public/assets/{data-DlmoKxmN.js → data-BGUAGVkV.js} +1 -1
  70. package/payload/server/public/assets/{graph-BVvcFSEQ.js → graph-MvYxZOBF.js} +1 -1
  71. package/payload/server/public/assets/{graph-labels-cCvgu38Y.js → graph-labels-D865qb3K.js} +1 -1
  72. package/payload/server/public/assets/{page-BnAxPkmg.js → page--hOVRrgN.js} +1 -1
  73. package/payload/server/public/assets/{page-BnaN3u86.js → page-C2b1nlOc.js} +1 -1
  74. package/payload/server/public/data.html +3 -3
  75. package/payload/server/public/graph.html +3 -3
  76. package/payload/server/public/index.html +4 -4
  77. package/payload/server/server.js +19 -83
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Task 144 — per-endpoint coverage for the 10 new PropertyData adapters.
3
+ * For each new endpoint, assert:
4
+ * 1. happy path — JSON parses and round-trips through the adapter
5
+ * 2. 4xx upstream → PropertyDataError with status preserved
6
+ * 3. non-JSON body → BadResponseError
7
+ * 4. missing key → KeyNotRegisteredError
8
+ *
9
+ * The existing propertydata-api.test.ts proves the four paths fire from
10
+ * `propertyDataCall`; this file proves each of the 10 adapter exports
11
+ * threads its own endpoint path through that same plumbing so the
12
+ * MCP tool actually hits the right URL.
13
+ */
14
+
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
+
17
+ // Mock the Neo4j layer keyed by accountId so each test can choose an
18
+ // already-registered account (happy / 4xx / bad-response) or a brand-new
19
+ // "ghost" account (missing-key) without ordering coupling.
20
+ vi.mock("../lib/neo4j.js", () => {
21
+ const stored = new Map<string, string>();
22
+ return {
23
+ getSession: () => ({
24
+ run: vi.fn(async (cypher: string, params: Record<string, unknown>) => {
25
+ const accountId = String(params.accountId ?? "");
26
+ if (/MATCH \(k:PropertyDataKey/.test(cypher) && /RETURN/.test(cypher)) {
27
+ const encryptedKey = stored.get(accountId);
28
+ return {
29
+ records: encryptedKey
30
+ ? [
31
+ {
32
+ get: (k: string) => {
33
+ if (k === "encryptedKey") return encryptedKey;
34
+ if (k === "createdAt") return "2026-01-01T00:00:00Z";
35
+ if (k === "updatedAt") return "2026-01-01T00:00:00Z";
36
+ return null;
37
+ },
38
+ },
39
+ ]
40
+ : [],
41
+ };
42
+ }
43
+ if (/MERGE \(k:PropertyDataKey/.test(cypher)) {
44
+ stored.set(accountId, String(params.encryptedKey));
45
+ return { records: [] };
46
+ }
47
+ if (/DELETE k/.test(cypher)) {
48
+ const had = stored.delete(accountId);
49
+ return {
50
+ records: [
51
+ {
52
+ get: (k: string) =>
53
+ k === "deleted" ? { toNumber: () => (had ? 1 : 0) } : null,
54
+ },
55
+ ],
56
+ };
57
+ }
58
+ return { records: [] };
59
+ }),
60
+ close: vi.fn(async () => {}),
61
+ }),
62
+ closeDriver: vi.fn(async () => {}),
63
+ };
64
+ });
65
+
66
+ vi.mock("../lib/crypto.js", () => ({
67
+ encrypt: (s: string) => `enc:${s}`,
68
+ decrypt: (s: string) => s.replace(/^enc:/, ""),
69
+ }));
70
+
71
+ import {
72
+ pricesPerSqf,
73
+ soldPricesPerSqf,
74
+ crime,
75
+ floodRisk,
76
+ councilTax,
77
+ planningApplications,
78
+ addressMatchUprn,
79
+ growthPsf,
80
+ demandRent,
81
+ propertyTypes,
82
+ } from "../tools/endpoints.js";
83
+ import {
84
+ KeyNotRegisteredError,
85
+ PropertyDataError,
86
+ BadResponseError,
87
+ } from "../lib/propertydata-api.js";
88
+ import { keyRegister } from "../tools/key-register.js";
89
+
90
+ const originalFetch = globalThis.fetch;
91
+
92
+ function mockFetch(impl: (url: string) => Response | Promise<Response>) {
93
+ const fn = vi.fn(async (url: string | URL | Request) => impl(String(url))) as unknown as typeof fetch;
94
+ globalThis.fetch = fn;
95
+ return fn as unknown as ReturnType<typeof vi.fn>;
96
+ }
97
+
98
+ async function registerTestKey(accountId: string) {
99
+ mockFetch(() => new Response(JSON.stringify({ status: "success", uk_hpi: 100 }), { status: 200 }));
100
+ await keyRegister({ apiKey: "test-key-12345", accountId });
101
+ }
102
+
103
+ beforeEach(() => {
104
+ // no-op
105
+ });
106
+
107
+ afterEach(() => {
108
+ globalThis.fetch = originalFetch;
109
+ vi.restoreAllMocks();
110
+ });
111
+
112
+ interface EndpointCase {
113
+ toolName: string;
114
+ endpointPath: string;
115
+ adapter: (args: { accountId: string } & Record<string, unknown>) => Promise<string>;
116
+ sampleParams: Record<string, unknown>;
117
+ happyPayload: Record<string, unknown>;
118
+ happyAssertField: string;
119
+ }
120
+
121
+ const cases: EndpointCase[] = [
122
+ {
123
+ toolName: "property-data-prices-per-sqf",
124
+ endpointPath: "prices-per-sqf",
125
+ adapter: pricesPerSqf,
126
+ sampleParams: { postcode: "CM23 2UJ" },
127
+ happyPayload: { status: "success", data: { avg: 450 } },
128
+ happyAssertField: "avg",
129
+ },
130
+ {
131
+ toolName: "property-data-sold-prices-per-sqf",
132
+ endpointPath: "sold-prices-per-sqf",
133
+ adapter: soldPricesPerSqf,
134
+ sampleParams: { postcode: "CM23 2UJ", max_age: 24 },
135
+ happyPayload: { status: "success", data: { avg: 425, sales: [] } },
136
+ happyAssertField: "avg",
137
+ },
138
+ {
139
+ toolName: "property-data-crime",
140
+ endpointPath: "crime",
141
+ adapter: crime,
142
+ sampleParams: { postcode: "CM23" },
143
+ happyPayload: { status: "success", data: { rate_per_1000: 42 } },
144
+ happyAssertField: "rate_per_1000",
145
+ },
146
+ {
147
+ toolName: "property-data-flood-risk",
148
+ endpointPath: "flood-risk",
149
+ adapter: floodRisk,
150
+ sampleParams: { postcode: "CM23 2UJ" },
151
+ happyPayload: { status: "success", data: { river: "very-low" } },
152
+ happyAssertField: "river",
153
+ },
154
+ {
155
+ toolName: "property-data-council-tax",
156
+ endpointPath: "council-tax",
157
+ adapter: councilTax,
158
+ sampleParams: { postcode: "CM23 2UJ" },
159
+ happyPayload: { status: "success", data: { council: "East Herts", band_d: 2100 } },
160
+ happyAssertField: "council",
161
+ },
162
+ {
163
+ toolName: "property-data-planning-applications",
164
+ endpointPath: "planning-applications",
165
+ adapter: planningApplications,
166
+ sampleParams: { postcode: "CM23", results: 10 },
167
+ happyPayload: { status: "success", data: { applications: [] } },
168
+ happyAssertField: "applications",
169
+ },
170
+ {
171
+ toolName: "property-data-address-match-uprn",
172
+ endpointPath: "address-match-uprn",
173
+ adapter: addressMatchUprn,
174
+ sampleParams: { address: "1 The High Street, Bishop's Stortford, CM23 2UJ" },
175
+ happyPayload: { status: "success", data: { uprn: "10001234567" } },
176
+ happyAssertField: "uprn",
177
+ },
178
+ {
179
+ toolName: "property-data-growth-psf",
180
+ endpointPath: "growth-psf",
181
+ adapter: growthPsf,
182
+ sampleParams: { postcode: "CM23" },
183
+ happyPayload: { status: "success", data: { series: [{ year: 2019, psf: 400 }] } },
184
+ happyAssertField: "series",
185
+ },
186
+ {
187
+ toolName: "property-data-demand-rent",
188
+ endpointPath: "demand-rent",
189
+ adapter: demandRent,
190
+ sampleParams: { postcode: "CM23", bedrooms: 3 },
191
+ happyPayload: { status: "success", data: { for_rent: 12, lets_per_month: 4 } },
192
+ happyAssertField: "for_rent",
193
+ },
194
+ {
195
+ toolName: "property-data-property-types",
196
+ endpointPath: "property-types",
197
+ adapter: propertyTypes,
198
+ sampleParams: { postcode: "CM23" },
199
+ happyPayload: { status: "success", data: { detached: 28, flat: 12 } },
200
+ happyAssertField: "detached",
201
+ },
202
+ ];
203
+
204
+ describe("Task 144 endpoint adapters", () => {
205
+ for (const c of cases) {
206
+ describe(c.toolName, () => {
207
+ const acct = `acct-${c.endpointPath}`;
208
+
209
+ it("happy path returns the parsed JSON payload", async () => {
210
+ await registerTestKey(acct);
211
+ let capturedUrl = "";
212
+ mockFetch((url) => {
213
+ capturedUrl = url;
214
+ return new Response(JSON.stringify(c.happyPayload), { status: 200 });
215
+ });
216
+ const text = await c.adapter({ accountId: acct, ...c.sampleParams });
217
+ expect(capturedUrl).toContain(`/${c.endpointPath}?`);
218
+ const parsed = JSON.parse(text);
219
+ // payload has shape { status, data: { ... } } — assert the field
220
+ // exists somewhere in the round-tripped output.
221
+ expect(JSON.stringify(parsed)).toContain(c.happyAssertField);
222
+ });
223
+
224
+ it("maps HTTP 4xx to PropertyDataError preserving the status", async () => {
225
+ await registerTestKey(acct);
226
+ mockFetch(() => new Response("bad request", { status: 400 }));
227
+ try {
228
+ await c.adapter({ accountId: acct, ...c.sampleParams });
229
+ throw new Error("expected PropertyDataError");
230
+ } catch (err) {
231
+ expect(err).toBeInstanceOf(PropertyDataError);
232
+ expect((err as PropertyDataError).status).toBe(400);
233
+ }
234
+ });
235
+
236
+ it("maps non-JSON body to BadResponseError", async () => {
237
+ await registerTestKey(acct);
238
+ mockFetch(() => new Response("<html>maintenance</html>", { status: 200 }));
239
+ await expect(
240
+ c.adapter({ accountId: acct, ...c.sampleParams }),
241
+ ).rejects.toBeInstanceOf(BadResponseError);
242
+ });
243
+
244
+ it("throws KeyNotRegisteredError when no key is stored", async () => {
245
+ // brand-new account with nothing registered
246
+ await expect(
247
+ c.adapter({ accountId: `${acct}-ghost`, ...c.sampleParams }),
248
+ ).rejects.toBeInstanceOf(KeyNotRegisteredError);
249
+ });
250
+ });
251
+ }
252
+ });
@@ -38,11 +38,25 @@ describe("schema parity", () => {
38
38
  expect(registered).toEqual(declared);
39
39
  });
40
40
 
41
- it("declares all 12 tools (3 key-mgmt + 9 endpoints)", () => {
41
+ it("declares all 22 tools (3 key-mgmt + 19 endpoints)", () => {
42
42
  const declared = readPluginMdTools();
43
- expect(declared).toHaveLength(12);
43
+ expect(declared).toHaveLength(22);
44
44
  expect(declared).toContain("property-data-key-register");
45
45
  expect(declared).toContain("property-data-valuation-sale");
46
46
  expect(declared).toContain("property-data-national-hpi");
47
+ for (const name of [
48
+ "property-data-prices-per-sqf",
49
+ "property-data-sold-prices-per-sqf",
50
+ "property-data-crime",
51
+ "property-data-flood-risk",
52
+ "property-data-council-tax",
53
+ "property-data-planning-applications",
54
+ "property-data-address-match-uprn",
55
+ "property-data-growth-psf",
56
+ "property-data-demand-rent",
57
+ "property-data-property-types",
58
+ ]) {
59
+ expect(declared).toContain(name);
60
+ }
47
61
  });
48
62
  });
@@ -17,6 +17,16 @@ import {
17
17
  yields,
18
18
  growth,
19
19
  nationalHpi,
20
+ pricesPerSqf,
21
+ soldPricesPerSqf,
22
+ crime,
23
+ floodRisk,
24
+ councilTax,
25
+ planningApplications,
26
+ addressMatchUprn,
27
+ growthPsf,
28
+ demandRent,
29
+ propertyTypes,
20
30
  } from "./tools/endpoints.js";
21
31
  import { closeDriver } from "./lib/neo4j.js";
22
32
  import {
@@ -236,6 +246,151 @@ server.tool(
236
246
  endpointHandler(nationalHpi),
237
247
  );
238
248
 
249
+ // ─────────────────────────────────────────────────────────────────
250
+ // Task 144 — £/sqft baselines and growth
251
+ // ─────────────────────────────────────────────────────────────────
252
+
253
+ const pricesPerSqfSchema = {
254
+ postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
255
+ type: z
256
+ .enum(["detached", "semi-detached", "terraced", "flat"])
257
+ .optional()
258
+ .describe("Filter by property type"),
259
+ bedrooms: z.number().int().min(0).optional().describe("Filter by bedroom count"),
260
+ };
261
+
262
+ server.tool(
263
+ "property-data-prices-per-sqf",
264
+ "Asking £/sqft distribution for sale listings in a postcode — avg plus 70/80/90/100% percentile ranges, with the raw listings that have square-foot data. Use for the asking £/sqft KPI on a market report. Costs 1 credit per call.",
265
+ pricesPerSqfSchema,
266
+ endpointHandler(pricesPerSqf),
267
+ );
268
+
269
+ server.tool(
270
+ "property-data-sold-prices-per-sqf",
271
+ "Land Registry sold £/sqft distribution for a postcode — avg plus percentile ranges, up to 20 raw sales with square-foot data and a sales-window date range. Use for the achieved £/sqft KPI. Costs 1 credit per call.",
272
+ {
273
+ postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
274
+ type: z
275
+ .enum(["detached", "semi-detached", "terraced", "flat"])
276
+ .optional()
277
+ .describe("Filter by property type"),
278
+ bedrooms: z.number().int().min(0).optional().describe("Filter by bedroom count"),
279
+ max_age: z.number().int().min(1).optional().describe("Max transaction age in months"),
280
+ },
281
+ endpointHandler(soldPricesPerSqf),
282
+ );
283
+
284
+ server.tool(
285
+ "property-data-growth-psf",
286
+ "7-year £/sqft series with year-on-year percent change for a postcode. Use for the £/sqft growth chart on a market report. Costs 1 credit per call.",
287
+ {
288
+ postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
289
+ type: z
290
+ .enum(["detached", "semi-detached", "terraced", "flat"])
291
+ .optional()
292
+ .describe("Filter by property type"),
293
+ },
294
+ endpointHandler(growthPsf),
295
+ );
296
+
297
+ // ─────────────────────────────────────────────────────────────────
298
+ // Task 144 — Area risk and cost of occupancy
299
+ // ─────────────────────────────────────────────────────────────────
300
+
301
+ server.tool(
302
+ "property-data-crime",
303
+ "Recent crime statistics for a postcode (or location / town) — per-1,000 rate, 12-month total, breakdown by crime type, and per-month observations. Use for the area-risk pillar. Costs 1 credit per call.",
304
+ {
305
+ postcode: z.string().min(2).optional().describe("UK postcode (outcode or full)"),
306
+ location: z
307
+ .string()
308
+ .optional()
309
+ .describe("Latitude,longitude pair (e.g. '51.5074,-0.1278'). Alternative to postcode."),
310
+ town: z.string().optional().describe("UK town name. Alternative to postcode."),
311
+ },
312
+ endpointHandler(crime),
313
+ );
314
+
315
+ server.tool(
316
+ "property-data-flood-risk",
317
+ "River and sea flood-risk rating for a full UK postcode. Use for area-risk reporting and lender-ready disclosures. Requires a full postcode (not outcode). Costs 1 credit per call.",
318
+ {
319
+ postcode: z.string().min(5).describe("Full UK postcode (not outcode)"),
320
+ },
321
+ endpointHandler(floodRisk),
322
+ );
323
+
324
+ server.tool(
325
+ "property-data-council-tax",
326
+ "Council-tax authority, rating, and per-band charges (A–H) for a full UK postcode, plus any per-property bands the API returns. Use for cost-of-occupancy disclosures. Requires a full postcode. Costs 1 credit per call.",
327
+ {
328
+ postcode: z.string().min(5).describe("Full UK postcode (not outcode)"),
329
+ },
330
+ endpointHandler(councilTax),
331
+ );
332
+
333
+ // ─────────────────────────────────────────────────────────────────
334
+ // Task 144 — Planning precedent and address resolution
335
+ // ─────────────────────────────────────────────────────────────────
336
+
337
+ server.tool(
338
+ "property-data-planning-applications",
339
+ "Planning applications within a radius of a postcode — references, proposals, decisions, appeals, distance. Use for development precedent and pre-purchase risk. Costs 2 credits per call. Throttled to ~4 calls per 10 seconds upstream — wrap in the preval skill for fan-out, do not loop directly.",
340
+ {
341
+ postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
342
+ decision_rating: z
343
+ .enum(["positive", "neutral", "negative"])
344
+ .optional()
345
+ .describe("Filter by decision sentiment"),
346
+ category: z.string().optional().describe("PropertyData planning category filter"),
347
+ max_age_decision: z
348
+ .number()
349
+ .int()
350
+ .min(1)
351
+ .optional()
352
+ .describe("Max age of decision in months"),
353
+ results: z.number().int().min(1).max(100).optional().describe("Max results to return"),
354
+ },
355
+ endpointHandler(planningApplications),
356
+ );
357
+
358
+ server.tool(
359
+ "property-data-address-match-uprn",
360
+ "Resolve a full UK address string to its UPRN, address classification, and lat/lng. Use for CRM hygiene and unique-property identification. Costs 10 credits per call — do not fan out blindly; call once per address you actually need to resolve.",
361
+ {
362
+ address: z.string().min(5).describe("Full UK address (line one + postcode)"),
363
+ },
364
+ endpointHandler(addressMatchUprn),
365
+ );
366
+
367
+ // ─────────────────────────────────────────────────────────────────
368
+ // Task 144 — Rental demand and property-type mix
369
+ // ─────────────────────────────────────────────────────────────────
370
+
371
+ server.tool(
372
+ "property-data-demand-rent",
373
+ "Rental demand signals for a postcode — for-rent count, lets per month, turnover percent, months of inventory, days on market, and a rating. Use for the rental pillar on a market report. Costs 1 credit per call.",
374
+ {
375
+ postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
376
+ bedrooms: z.number().int().min(0).optional().describe("Filter by bedroom count"),
377
+ type: z
378
+ .enum(["detached", "semi-detached", "terraced", "flat"])
379
+ .optional()
380
+ .describe("Filter by property type"),
381
+ },
382
+ endpointHandler(demandRent),
383
+ );
384
+
385
+ server.tool(
386
+ "property-data-property-types",
387
+ "Output-area property-type distribution (percent by class — detached, semi-detached, terraced, flat) for a postcode. Use for the property-type mix doughnut on a market report. Costs 1 credit per call.",
388
+ {
389
+ postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
390
+ },
391
+ endpointHandler(propertyTypes),
392
+ );
393
+
239
394
  // ─────────────────────────────────────────────────────────────────
240
395
  // Lifecycle
241
396
  // ─────────────────────────────────────────────────────────────────
@@ -25,3 +25,23 @@ export const demand = (a: Args) => call("property-data-demand", "demand", a);
25
25
  export const yields = (a: Args) => call("property-data-yields", "yields", a);
26
26
  export const growth = (a: Args) => call("property-data-growth", "growth", a);
27
27
  export const nationalHpi = (a: Args) => call("property-data-national-hpi", "national-hpi", a);
28
+
29
+ // Task 144 — 10 additional read endpoints (£/sqft, area risk,
30
+ // planning precedent, UPRN, demand-rent, property-type mix).
31
+ // All inherit the same observability + error contract as above.
32
+
33
+ export const pricesPerSqf = (a: Args) =>
34
+ call("property-data-prices-per-sqf", "prices-per-sqf", a);
35
+ export const soldPricesPerSqf = (a: Args) =>
36
+ call("property-data-sold-prices-per-sqf", "sold-prices-per-sqf", a);
37
+ export const crime = (a: Args) => call("property-data-crime", "crime", a);
38
+ export const floodRisk = (a: Args) => call("property-data-flood-risk", "flood-risk", a);
39
+ export const councilTax = (a: Args) => call("property-data-council-tax", "council-tax", a);
40
+ export const planningApplications = (a: Args) =>
41
+ call("property-data-planning-applications", "planning-applications", a);
42
+ export const addressMatchUprn = (a: Args) =>
43
+ call("property-data-address-match-uprn", "address-match-uprn", a);
44
+ export const growthPsf = (a: Args) => call("property-data-growth-psf", "growth-psf", a);
45
+ export const demandRent = (a: Args) => call("property-data-demand-rent", "demand-rent", a);
46
+ export const propertyTypes = (a: Args) =>
47
+ call("property-data-property-types", "property-types", a);
@@ -18,6 +18,15 @@ Trigger phrases:
18
18
  - "what's it worth — [postcode], [type], [beds]"
19
19
  - "sold prices on [street|postcode]"
20
20
  - "growth in [postcode]"
21
+ - "£/sqft in [postcode]" / "price per square foot in [postcode]"
22
+ - "£/sqft growth in [postcode]"
23
+ - "crime in [postcode|town]"
24
+ - "flood risk for [postcode]"
25
+ - "council tax band for [postcode]"
26
+ - "planning applications near [postcode]"
27
+ - "what's the UPRN of [address]"
28
+ - "rental demand in [postcode]"
29
+ - "property-type mix in [postcode]"
21
30
 
22
31
  ## Inputs required
23
32
 
@@ -39,6 +48,16 @@ Trigger phrases:
39
48
  | `property-data-yields` | Gross rental yield | postcode, bedrooms |
40
49
  | `property-data-growth` | Short and long-term price growth | postcode |
41
50
  | `property-data-national-hpi` | UK national HPI series | (none) |
51
+ | `property-data-prices-per-sqf` | Asking £/sqft distribution | postcode (type, bedrooms optional) |
52
+ | `property-data-sold-prices-per-sqf` | Achieved £/sqft distribution | postcode (type, bedrooms, max_age optional) |
53
+ | `property-data-growth-psf` | 7-year £/sqft series with YoY% | postcode (type optional) |
54
+ | `property-data-crime` | Crime rate + breakdown | postcode, or location, or town |
55
+ | `property-data-flood-risk` | River + sea flood rating | full postcode |
56
+ | `property-data-council-tax` | Council + A–H band charges | full postcode |
57
+ | `property-data-planning-applications` | Planning precedent (2 credits/call) | postcode (decision_rating, category, max_age_decision, results optional) |
58
+ | `property-data-address-match-uprn` | Address → UPRN (10 credits/call) | address |
59
+ | `property-data-demand-rent` | Rental demand signals | postcode (bedrooms, type optional) |
60
+ | `property-data-property-types` | Output-area type distribution | postcode |
42
61
 
43
62
  ## Output
44
63
 
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAK3B,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAA0E,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAIzH,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,iBAAiB,CAAA;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AA8E3D,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,gBAAgB,GAAG,SAAS,CAAC;IACvF,KAAK,EAAE,YAAY,CAAA;IACnB,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;CAC/B;AAgSD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CA2wBjD"}
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAA0E,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAIzH,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,iBAAiB,CAAA;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AA6E3D,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,gBAAgB,GAAG,SAAS,CAAC;IACvF,KAAK,EAAE,YAAY,CAAA;IACnB,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;CAC/B;AAoHD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAusBjD"}