@rubytech/create-maxy-code 0.1.44 → 0.1.47

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