@rubytech/create-maxy-code 0.1.58 → 0.1.60
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/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +30 -19
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +35 -90
- package/payload/platform/plugins/docs/references/admin-session.md +17 -16
- package/payload/platform/plugins/docs/references/platform.md +3 -3
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/loop/PLUGIN.md +13 -1
- package/payload/platform/plugins/loop/mcp/dist/index.js +8 -7
- package/payload/platform/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.js +48 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.d.ts +22 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.js +165 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.d.ts +1 -1
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.js +4 -39
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/platform/plugins/loop/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-register.js +11 -28
- package/payload/platform/plugins/loop/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/src/index.ts +10 -7
- package/payload/platform/plugins/loop/mcp/src/lib/file-crypto.ts +54 -0
- package/payload/platform/plugins/loop/mcp/src/lib/key-store.ts +221 -0
- package/payload/platform/plugins/loop/mcp/src/lib/loop-api.ts +5 -42
- package/payload/platform/plugins/loop/mcp/src/tools/key-deregister.ts +5 -20
- package/payload/platform/plugins/loop/mcp/src/tools/key-register.ts +14 -39
- package/payload/platform/plugins/preval/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/preval/PLUGIN.md +39 -0
- package/payload/platform/plugins/preval/skills/property-preval/SKILL.md +78 -0
- package/payload/platform/plugins/preval/skills/property-preval/references/render.py +265 -0
- package/payload/platform/plugins/preval/skills/property-preval/references/template-inputs.schema.json +143 -0
- package/payload/platform/plugins/preval/skills/property-preval/references/template.html +253 -0
- package/payload/platform/plugins/property-data/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/property-data/PLUGIN.md +60 -2
- package/payload/platform/plugins/property-data/mcp/dist/index.js +95 -12
- package/payload/platform/plugins/property-data/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.js +49 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.d.ts +15 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.js +135 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts +0 -1
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.js +11 -22
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.d.ts +10 -0
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.js +15 -2
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-register.js +30 -23
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/src/__tests__/endpoints.test.ts +252 -0
- package/payload/platform/plugins/property-data/mcp/src/__tests__/schema-parity.test.ts +16 -2
- package/payload/platform/plugins/property-data/mcp/src/index.ts +171 -11
- package/payload/platform/plugins/property-data/mcp/src/lib/file-crypto.ts +55 -0
- package/payload/platform/plugins/property-data/mcp/src/lib/key-store.ts +181 -0
- package/payload/platform/plugins/property-data/mcp/src/lib/propertydata-api.ts +9 -24
- package/payload/platform/plugins/property-data/mcp/src/tools/endpoints.ts +22 -2
- package/payload/platform/plugins/property-data/mcp/src/tools/key-deregister.ts +5 -17
- package/payload/platform/plugins/property-data/mcp/src/tools/key-register.ts +29 -33
- package/payload/platform/plugins/property-data/skills/property-data/SKILL.md +19 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +25 -260
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +3 -7
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +6 -32
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +6 -24
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +18 -91
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.js +0 -11
- package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts +0 -21
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-store.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/platform/templates/agents/admin/SOUL.md +1 -1
- package/payload/premium-plugins/real-agent/BUNDLE.md +5 -3
- package/payload/premium-plugins/real-agent/agents/negotiator.md +1 -1
- package/payload/premium-plugins/real-agent/agents/valuer.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/premium-plugins/real-agent/plugins/loop/PLUGIN.md +13 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js +8 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.js +48 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.d.ts +22 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.js +165 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.d.ts +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.js +4 -39
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-register.js +11 -28
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/index.ts +10 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/file-crypto.ts +54 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/key-store.ts +221 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/loop-api.ts +5 -42
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/key-deregister.ts +5 -20
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/key-register.ts +14 -39
- package/payload/premium-plugins/real-agent/plugins/preval/.claude-plugin/plugin.json +8 -0
- package/payload/premium-plugins/real-agent/plugins/preval/PLUGIN.md +39 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/SKILL.md +78 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/render.py +265 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template-inputs.schema.json +143 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template.html +253 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/.claude-plugin/plugin.json +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/PLUGIN.md +60 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/index.js +95 -12
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.js +49 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.d.ts +15 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.js +135 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts +0 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.js +11 -22
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.d.ts +10 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.js +15 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-register.js +30 -23
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/__tests__/endpoints.test.ts +252 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/__tests__/schema-parity.test.ts +16 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/index.ts +171 -11
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/lib/file-crypto.ts +55 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/lib/key-store.ts +181 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/lib/propertydata-api.ts +9 -24
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/tools/endpoints.ts +22 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/tools/key-deregister.ts +5 -17
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/tools/key-register.ts +29 -33
- package/payload/premium-plugins/real-agent/plugins/property-data/skills/property-data/SKILL.md +19 -0
- package/payload/server/public/assets/{admin-2ZJmzJ_g.js → admin-Bp-BjBCX.js} +51 -51
- package/payload/server/public/assets/{data-DlmoKxmN.js → data-BGUAGVkV.js} +1 -1
- package/payload/server/public/assets/{graph-BVvcFSEQ.js → graph-MvYxZOBF.js} +1 -1
- package/payload/server/public/assets/{graph-labels-cCvgu38Y.js → graph-labels-D865qb3K.js} +1 -1
- package/payload/server/public/assets/{page-BnAxPkmg.js → page--hOVRrgN.js} +1 -1
- package/payload/server/public/assets/{page-BnaN3u86.js → page-C2b1nlOc.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +18 -83
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.d.ts +0 -51
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.d.ts.map +0 -1
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.js +0 -196
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.js.map +0 -1
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/package.json +0 -7
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { resolve, dirname } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { getSession } from "./neo4j.js";
|
|
5
|
+
import { encrypt, decrypt } from "./crypto.js";
|
|
6
|
+
import { encryptWith, decryptWith } from "./file-crypto.js";
|
|
7
|
+
|
|
8
|
+
// Storage abstraction so the same MCP body runs against Neo4j (platform mode)
|
|
9
|
+
// or an on-disk encrypted file (standalone mode, spawned by Claude Code from
|
|
10
|
+
// ~/.claude/). Selected by LOOP_STANDALONE at boot. Loop is multi-team — each
|
|
11
|
+
// account can register many team keys — so the file backend stores a nested
|
|
12
|
+
// map and the store API mirrors that.
|
|
13
|
+
|
|
14
|
+
export interface LoopStoredKey {
|
|
15
|
+
teamName: string;
|
|
16
|
+
apiKey: string;
|
|
17
|
+
teamAddress: string;
|
|
18
|
+
agentId: string;
|
|
19
|
+
permissions: string[];
|
|
20
|
+
createdAt: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type LoopKeyInput = Omit<LoopStoredKey, "createdAt" | "updatedAt">;
|
|
25
|
+
|
|
26
|
+
export interface LoopKeyStore {
|
|
27
|
+
readonly backendName: string;
|
|
28
|
+
readonly backendPath: string;
|
|
29
|
+
loadAll(accountId: string): Promise<LoopStoredKey[]>;
|
|
30
|
+
save(accountId: string, key: LoopKeyInput): Promise<{ created: boolean }>;
|
|
31
|
+
delete(accountId: string, teamName: string): Promise<boolean>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class Neo4jLoopKeyStore implements LoopKeyStore {
|
|
35
|
+
readonly backendName = "neo4j";
|
|
36
|
+
readonly backendPath = process.env.NEO4J_URI ?? "<unset>";
|
|
37
|
+
|
|
38
|
+
async loadAll(accountId: string): Promise<LoopStoredKey[]> {
|
|
39
|
+
const session = getSession();
|
|
40
|
+
try {
|
|
41
|
+
const result = await session.run(
|
|
42
|
+
`MATCH (k:LoopTeamKey {accountId: $accountId})
|
|
43
|
+
RETURN k.teamName AS teamName, k.encryptedKey AS encryptedKey,
|
|
44
|
+
k.teamAddress AS teamAddress, k.agentId AS agentId,
|
|
45
|
+
k.permissions AS permissions, k.createdAt AS createdAt,
|
|
46
|
+
k.updatedAt AS updatedAt
|
|
47
|
+
ORDER BY k.teamName`,
|
|
48
|
+
{ accountId },
|
|
49
|
+
);
|
|
50
|
+
return result.records.map((r) => ({
|
|
51
|
+
teamName: r.get("teamName"),
|
|
52
|
+
apiKey: decrypt(r.get("encryptedKey")),
|
|
53
|
+
teamAddress: r.get("teamAddress") ?? "",
|
|
54
|
+
agentId: r.get("agentId") ?? "",
|
|
55
|
+
permissions: r.get("permissions") ?? [],
|
|
56
|
+
createdAt: r.get("createdAt") ?? "",
|
|
57
|
+
updatedAt: r.get("updatedAt") ?? "",
|
|
58
|
+
}));
|
|
59
|
+
} finally {
|
|
60
|
+
await session.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async save(accountId: string, key: LoopKeyInput): Promise<{ created: boolean }> {
|
|
65
|
+
const encryptedKey = encrypt(key.apiKey);
|
|
66
|
+
const now = new Date().toISOString();
|
|
67
|
+
const session = getSession();
|
|
68
|
+
try {
|
|
69
|
+
await session.run(
|
|
70
|
+
`CREATE INDEX loop_team_key_account_team IF NOT EXISTS
|
|
71
|
+
FOR (k:LoopTeamKey) ON (k.accountId, k.teamName)`,
|
|
72
|
+
{},
|
|
73
|
+
);
|
|
74
|
+
const result = await session.run(
|
|
75
|
+
`MERGE (k:LoopTeamKey {teamName: $teamName, accountId: $accountId})
|
|
76
|
+
ON CREATE SET
|
|
77
|
+
k.encryptedKey = $encryptedKey,
|
|
78
|
+
k.teamAddress = $teamAddress,
|
|
79
|
+
k.agentId = $agentId,
|
|
80
|
+
k.permissions = $permissions,
|
|
81
|
+
k.createdAt = $now,
|
|
82
|
+
k.updatedAt = $now
|
|
83
|
+
RETURN k.createdAt = $now AS created`,
|
|
84
|
+
{
|
|
85
|
+
teamName: key.teamName,
|
|
86
|
+
encryptedKey,
|
|
87
|
+
teamAddress: key.teamAddress,
|
|
88
|
+
agentId: key.agentId,
|
|
89
|
+
accountId,
|
|
90
|
+
permissions: key.permissions,
|
|
91
|
+
now,
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
return { created: Boolean(result.records[0]?.get("created")) };
|
|
95
|
+
} finally {
|
|
96
|
+
await session.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async delete(accountId: string, teamName: string): Promise<boolean> {
|
|
101
|
+
const session = getSession();
|
|
102
|
+
try {
|
|
103
|
+
const result = await session.run(
|
|
104
|
+
`MATCH (k:LoopTeamKey {teamName: $teamName, accountId: $accountId})
|
|
105
|
+
DELETE k
|
|
106
|
+
RETURN count(k) AS deleted`,
|
|
107
|
+
{ teamName, accountId },
|
|
108
|
+
);
|
|
109
|
+
const deleted = result.records[0]?.get("deleted")?.toNumber?.() ?? 0;
|
|
110
|
+
return deleted > 0;
|
|
111
|
+
} finally {
|
|
112
|
+
await session.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const LABEL = "loop";
|
|
118
|
+
|
|
119
|
+
interface FileEnvelope {
|
|
120
|
+
[accountId: string]: {
|
|
121
|
+
[teamName: string]: {
|
|
122
|
+
encryptedKey: string;
|
|
123
|
+
teamAddress: string;
|
|
124
|
+
agentId: string;
|
|
125
|
+
permissions: string[];
|
|
126
|
+
createdAt: string;
|
|
127
|
+
updatedAt: string;
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class FileLoopKeyStore implements LoopKeyStore {
|
|
133
|
+
readonly backendName = "file";
|
|
134
|
+
readonly backendPath: string;
|
|
135
|
+
private readonly storePath: string;
|
|
136
|
+
private readonly keyFilePath: string;
|
|
137
|
+
|
|
138
|
+
constructor() {
|
|
139
|
+
const home = homedir();
|
|
140
|
+
this.storePath = resolve(home, ".claude/.realagent-loop-key.enc");
|
|
141
|
+
this.keyFilePath = resolve(home, ".claude/.realagent-loop-encryption-key");
|
|
142
|
+
this.backendPath = this.storePath;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private ensureDir(): void {
|
|
146
|
+
mkdirSync(dirname(this.storePath), { recursive: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private readAll(): FileEnvelope {
|
|
150
|
+
if (!existsSync(this.storePath)) return {};
|
|
151
|
+
const blob = readFileSync(this.storePath, "utf-8").trim();
|
|
152
|
+
if (!blob) return {};
|
|
153
|
+
const json = decryptWith(blob, this.keyFilePath, LABEL);
|
|
154
|
+
return JSON.parse(json) as FileEnvelope;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private writeAll(env: FileEnvelope): void {
|
|
158
|
+
this.ensureDir();
|
|
159
|
+
const blob = encryptWith(JSON.stringify(env), this.keyFilePath, LABEL);
|
|
160
|
+
writeFileSync(this.storePath, blob, { mode: 0o600 });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async loadAll(accountId: string): Promise<LoopStoredKey[]> {
|
|
164
|
+
const env = this.readAll();
|
|
165
|
+
const account = env[accountId];
|
|
166
|
+
if (!account) return [];
|
|
167
|
+
return Object.entries(account)
|
|
168
|
+
.map(([teamName, entry]) => ({
|
|
169
|
+
teamName,
|
|
170
|
+
apiKey: decryptWith(entry.encryptedKey, this.keyFilePath, LABEL),
|
|
171
|
+
teamAddress: entry.teamAddress,
|
|
172
|
+
agentId: entry.agentId,
|
|
173
|
+
permissions: entry.permissions,
|
|
174
|
+
createdAt: entry.createdAt,
|
|
175
|
+
updatedAt: entry.updatedAt,
|
|
176
|
+
}))
|
|
177
|
+
.sort((a, b) => a.teamName.localeCompare(b.teamName));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async save(accountId: string, key: LoopKeyInput): Promise<{ created: boolean }> {
|
|
181
|
+
const env = this.readAll();
|
|
182
|
+
const account = env[accountId] ?? {};
|
|
183
|
+
const prev = account[key.teamName];
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
const encryptedKey = encryptWith(key.apiKey, this.keyFilePath, LABEL);
|
|
186
|
+
account[key.teamName] = {
|
|
187
|
+
encryptedKey,
|
|
188
|
+
teamAddress: key.teamAddress,
|
|
189
|
+
agentId: key.agentId,
|
|
190
|
+
permissions: key.permissions,
|
|
191
|
+
createdAt: prev?.createdAt ?? now,
|
|
192
|
+
updatedAt: now,
|
|
193
|
+
};
|
|
194
|
+
env[accountId] = account;
|
|
195
|
+
this.writeAll(env);
|
|
196
|
+
return { created: !prev };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async delete(accountId: string, teamName: string): Promise<boolean> {
|
|
200
|
+
const env = this.readAll();
|
|
201
|
+
const account = env[accountId];
|
|
202
|
+
if (!account || !(teamName in account)) return false;
|
|
203
|
+
delete account[teamName];
|
|
204
|
+
if (Object.keys(account).length === 0) delete env[accountId];
|
|
205
|
+
else env[accountId] = account;
|
|
206
|
+
this.writeAll(env);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let cached: LoopKeyStore | null = null;
|
|
212
|
+
|
|
213
|
+
export function selectLoopKeyStore(): LoopKeyStore {
|
|
214
|
+
if (cached) return cached;
|
|
215
|
+
cached = process.env.LOOP_STANDALONE === "1" ? new FileLoopKeyStore() : new Neo4jLoopKeyStore();
|
|
216
|
+
return cached;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function _resetKeyStoreForTests(): void {
|
|
220
|
+
cached = null;
|
|
221
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { decrypt } from "./crypto.js";
|
|
1
|
+
import { selectLoopKeyStore } from "./key-store.js";
|
|
3
2
|
|
|
4
3
|
// ─────────────────────────────────────────────────────────────────
|
|
5
4
|
// Loop API V2 HTTP Client + Cross-Team Aggregation Engine
|
|
@@ -90,7 +89,7 @@ function unwrapEnvelope<T>(parsed: unknown, toolName: string, teamName: string):
|
|
|
90
89
|
|
|
91
90
|
export interface LoopTeamKey {
|
|
92
91
|
teamName: string;
|
|
93
|
-
|
|
92
|
+
apiKey: string;
|
|
94
93
|
teamAddress: string;
|
|
95
94
|
agentId: string;
|
|
96
95
|
permissions: string[];
|
|
@@ -120,29 +119,7 @@ export interface AggregationResult<T> {
|
|
|
120
119
|
* Load all LoopTeamKey nodes for an account from Neo4j.
|
|
121
120
|
*/
|
|
122
121
|
export async function loadTeamKeys(accountId: string): Promise<LoopTeamKey[]> {
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const result = await session.run(
|
|
126
|
-
`MATCH (k:LoopTeamKey {accountId: $accountId})
|
|
127
|
-
RETURN k.teamName AS teamName, k.encryptedKey AS encryptedKey,
|
|
128
|
-
k.teamAddress AS teamAddress, k.agentId AS agentId,
|
|
129
|
-
k.permissions AS permissions, k.createdAt AS createdAt,
|
|
130
|
-
k.updatedAt AS updatedAt
|
|
131
|
-
ORDER BY k.teamName`,
|
|
132
|
-
{ accountId }
|
|
133
|
-
);
|
|
134
|
-
return result.records.map((r) => ({
|
|
135
|
-
teamName: r.get("teamName"),
|
|
136
|
-
encryptedKey: r.get("encryptedKey"),
|
|
137
|
-
teamAddress: r.get("teamAddress") ?? "",
|
|
138
|
-
agentId: r.get("agentId") ?? "",
|
|
139
|
-
permissions: r.get("permissions") ?? [],
|
|
140
|
-
createdAt: r.get("createdAt") ?? "",
|
|
141
|
-
updatedAt: r.get("updatedAt") ?? "",
|
|
142
|
-
}));
|
|
143
|
-
} finally {
|
|
144
|
-
await session.close();
|
|
145
|
-
}
|
|
122
|
+
return selectLoopKeyStore().loadAll(accountId);
|
|
146
123
|
}
|
|
147
124
|
|
|
148
125
|
// Truncate to 200 chars, escape newlines, single line — `[loop] <tool> team=<t>
|
|
@@ -396,20 +373,7 @@ export async function aggregateAcrossTeams<T>(
|
|
|
396
373
|
continue;
|
|
397
374
|
}
|
|
398
375
|
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
apiKey = decrypt(key.encryptedKey);
|
|
402
|
-
} catch (err) {
|
|
403
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
404
|
-
console.error(`[loop] decrypt failed for team=${key.teamName}: ${msg}`);
|
|
405
|
-
failures.push({
|
|
406
|
-
teamName: key.teamName,
|
|
407
|
-
reason: "Decryption failed",
|
|
408
|
-
kind: "other",
|
|
409
|
-
});
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
|
|
376
|
+
const apiKey = key.apiKey;
|
|
413
377
|
const teamName = key.teamName;
|
|
414
378
|
tasks.push({
|
|
415
379
|
key,
|
|
@@ -512,8 +476,7 @@ export async function withTeamKey<T>(
|
|
|
512
476
|
);
|
|
513
477
|
}
|
|
514
478
|
|
|
515
|
-
|
|
516
|
-
return execute(apiKey);
|
|
479
|
+
return execute(key.apiKey);
|
|
517
480
|
}
|
|
518
481
|
|
|
519
482
|
/**
|
|
@@ -1,27 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { selectLoopKeyStore } from "../lib/key-store.js";
|
|
2
2
|
|
|
3
3
|
export async function keyDeregister(params: {
|
|
4
4
|
teamName: string;
|
|
5
5
|
accountId: string;
|
|
6
6
|
}): Promise<void> {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
const result = await session.run(
|
|
12
|
-
`MATCH (k:LoopTeamKey {teamName: $teamName, accountId: $accountId})
|
|
13
|
-
DELETE k
|
|
14
|
-
RETURN count(k) AS deleted`,
|
|
15
|
-
{ teamName, accountId }
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
const deleted = result.records[0]?.get("deleted")?.toNumber?.() ?? 0;
|
|
19
|
-
if (deleted === 0) {
|
|
20
|
-
throw new Error(`Team "${teamName}" not found.`);
|
|
21
|
-
}
|
|
22
|
-
} finally {
|
|
23
|
-
await session.close();
|
|
7
|
+
const deleted = await selectLoopKeyStore().delete(params.accountId, params.teamName);
|
|
8
|
+
if (!deleted) {
|
|
9
|
+
throw new Error(`Team "${params.teamName}" not found.`);
|
|
24
10
|
}
|
|
25
|
-
|
|
26
|
-
console.error(`[loop] key-deregister team=${teamName} account=${accountId}`);
|
|
11
|
+
console.error(`[loop] key-deregister team=${params.teamName} account=${params.accountId}`);
|
|
27
12
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { encrypt } from "../lib/crypto.js";
|
|
1
|
+
import { selectLoopKeyStore } from "../lib/key-store.js";
|
|
3
2
|
import { loopGet, LoopApiError } from "../lib/loop-api.js";
|
|
4
3
|
|
|
5
4
|
interface LoopTeamResponse {
|
|
@@ -21,7 +20,7 @@ export async function keyRegister(params: {
|
|
|
21
20
|
const { teamName, apiKey, accountId } = params;
|
|
22
21
|
const permissions = params.permissions ?? ALL_PERMISSIONS;
|
|
23
22
|
|
|
24
|
-
// Validate key against Loop API — try /team endpoint
|
|
23
|
+
// Validate key against Loop API — try /team endpoint.
|
|
25
24
|
let teamAddress = "";
|
|
26
25
|
let agentId = "";
|
|
27
26
|
let warning: string | undefined;
|
|
@@ -31,11 +30,10 @@ export async function keyRegister(params: {
|
|
|
31
30
|
apiKey,
|
|
32
31
|
"/team",
|
|
33
32
|
"loop-key-register",
|
|
34
|
-
teamName
|
|
33
|
+
teamName,
|
|
35
34
|
);
|
|
36
35
|
|
|
37
36
|
if (Array.isArray(teamData) && teamData.length === 0) {
|
|
38
|
-
// 204 was converted to empty array — ambiguous scope
|
|
39
37
|
warning =
|
|
40
38
|
"Key accepted but Loop returned no team data (204). " +
|
|
41
39
|
"Verify the key's scope in Loop's dashboard.";
|
|
@@ -47,49 +45,26 @@ export async function keyRegister(params: {
|
|
|
47
45
|
if (err instanceof LoopApiError && err.status === 401) {
|
|
48
46
|
throw new Error(
|
|
49
47
|
`Loop API rejected the key for team "${teamName}" (HTTP 401). ` +
|
|
50
|
-
|
|
48
|
+
`Check that the key is valid and not expired.`,
|
|
51
49
|
);
|
|
52
50
|
}
|
|
53
|
-
// Network/timeout errors — still reject, key is unverified
|
|
54
51
|
throw new Error(
|
|
55
|
-
`Could not validate key against Loop API: ${err instanceof Error ? err.message : String(err)}
|
|
52
|
+
`Could not validate key against Loop API: ${err instanceof Error ? err.message : String(err)}`,
|
|
56
53
|
);
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
const { created } = await selectLoopKeyStore().save(accountId, {
|
|
57
|
+
teamName,
|
|
58
|
+
apiKey,
|
|
59
|
+
teamAddress,
|
|
60
|
+
agentId,
|
|
61
|
+
permissions,
|
|
62
|
+
});
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
await writeSession.run(
|
|
66
|
-
`CREATE INDEX loop_team_key_account_team IF NOT EXISTS
|
|
67
|
-
FOR (k:LoopTeamKey) ON (k.accountId, k.teamName)`,
|
|
68
|
-
{}
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const mergeResult = await writeSession.run(
|
|
72
|
-
`MERGE (k:LoopTeamKey {teamName: $teamName, accountId: $accountId})
|
|
73
|
-
ON CREATE SET
|
|
74
|
-
k.encryptedKey = $encryptedKey,
|
|
75
|
-
k.teamAddress = $teamAddress,
|
|
76
|
-
k.agentId = $agentId,
|
|
77
|
-
k.permissions = $permissions,
|
|
78
|
-
k.createdAt = $now,
|
|
79
|
-
k.updatedAt = $now
|
|
80
|
-
RETURN k.createdAt = $now AS created`,
|
|
81
|
-
{ teamName, encryptedKey, teamAddress, agentId, accountId, permissions, now }
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const created = mergeResult.records[0]?.get("created");
|
|
85
|
-
if (!created) {
|
|
86
|
-
throw new Error(`Team "${teamName}" is already registered for this account.`);
|
|
87
|
-
}
|
|
88
|
-
} finally {
|
|
89
|
-
await writeSession.close();
|
|
64
|
+
if (!created) {
|
|
65
|
+
throw new Error(`Team "${teamName}" is already registered for this account.`);
|
|
90
66
|
}
|
|
91
67
|
|
|
92
68
|
console.error(`[loop] key-register team=${teamName} account=${accountId}`);
|
|
93
|
-
|
|
94
69
|
return { warning };
|
|
95
70
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "real-agent-preval",
|
|
3
|
+
"description": "Single-address pre-valuation pack. One full UK address in, 4-page A4 PDF out (cover · valuation · area · demand+trend+sources). Consumes the real-agent-property-data MCP — no MCP tools of its own.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Rubytech LLC"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: real-agent-preval
|
|
3
|
+
description: "Single-address pre-valuation pack. One full UK address in, 4-page A4 PDF out (cover · valuation · area · demand+trend+sources). Consumes the real-agent-property-data MCP — no MCP tools of its own."
|
|
4
|
+
skills:
|
|
5
|
+
- skills/property-preval/SKILL.md
|
|
6
|
+
always: false
|
|
7
|
+
metadata: {"platform":{"optional":true,"embed":["admin"]}}
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Real Agent — Preval
|
|
11
|
+
|
|
12
|
+
Consumer sub-plugin. No MCP server, no new tools — only a skill (`property-preval`) that takes one full UK address and renders a 4-page A4 pre-valuation PDF by fanning out to 12 of the 22 `real-agent-property-data` MCP tools.
|
|
13
|
+
|
|
14
|
+
## What it produces
|
|
15
|
+
|
|
16
|
+
A 4-page A4 PDF and the source HTML, written to `~/Documents/Maxy/preval/<postcode>-<YYYY-MM-DD>/`:
|
|
17
|
+
|
|
18
|
+
- **Page 1 — Cover.** Full address, postcode chip, generated-date meta strip.
|
|
19
|
+
- **Page 2 — Valuation.** Average asking £, asking £/sqft, sold avg £, sold £/sqft. Nearest-sold comps table. Live asking comps grid.
|
|
20
|
+
- **Page 3 — Area.** Crime rating, flood risk, council-tax band D cost, local area population. Crime breakdown panel + council-tax band table. Headline read.
|
|
21
|
+
- **Page 4 — Demand, trend, sources.** Demand rating, days-on-market, turnover, for-sale count. 7y growth sparkline (£ and £/sqft). Rental-side mini-panel. Property-types doughnut. Sources/methodology block.
|
|
22
|
+
|
|
23
|
+
## How the address routes
|
|
24
|
+
|
|
25
|
+
The skill derives the postcode from the address (UK-postcode regex on the final token group), then fans out to **12** `real-agent-property-data` MCP tools in fixed call order:
|
|
26
|
+
|
|
27
|
+
`prices`, `prices-per-sqf`, `sold-prices`, `sold-prices-per-sqf`, `growth`, `growth-psf`, `demand`, `demand-rent`, `property-types`, `crime`, `flood-risk`, `council-tax`.
|
|
28
|
+
|
|
29
|
+
Two tools from the same MCP are **deliberately not called** by this skill:
|
|
30
|
+
|
|
31
|
+
- `property-data-address-match-uprn` — UPRN is CRM hygiene, not preval data.
|
|
32
|
+
- `property-data-planning-applications` — planning is a separate dev-precedent report.
|
|
33
|
+
|
|
34
|
+
Operators can still reach those tools via the underlying `property-data` skill or directly through the MCP.
|
|
35
|
+
|
|
36
|
+
## Dependencies
|
|
37
|
+
|
|
38
|
+
- Requires `real-agent-property-data` enabled on the account with a registered PropertyData API key (`property-data-key-register`). Without a key every fan-out call returns `Error: key-not-registered` and the renderer raises before producing a PDF.
|
|
39
|
+
- Requires Google Chrome at `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` (mac, headless `--print-to-pdf`). Same Chrome dependency as `property-market-report`.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: property-preval
|
|
3
|
+
description: Use when an operator asks for a pre-valuation pack for a single UK property — input is one full UK address, output is a 4-page A4 PDF covering valuation, area, and demand/trend. Triggers include "preval <address>", "pre-valuation for <address>", "what's <address> worth", "valuation pack for <address>", "market and area report for <address>".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Property Preval
|
|
7
|
+
|
|
8
|
+
Outcome: turn **one full UK address** into a 4-page A4 pre-valuation PDF the operator can send to a vendor before the appraisal visit. The skill owns the fan-out and the render; the operator supplies only the address.
|
|
9
|
+
|
|
10
|
+
## Input
|
|
11
|
+
|
|
12
|
+
A single string — the full UK address, ending in a valid UK postcode. Examples:
|
|
13
|
+
|
|
14
|
+
- `47 Phoenix Drive, Bishop's Stortford, CM23 2UJ`
|
|
15
|
+
- `Flat 3, 122 Kensington High Street, London, W8 4SG`
|
|
16
|
+
|
|
17
|
+
The skill derives the postcode by matching the UK postcode regex on the final token group. If no postcode is matched, the skill stops and asks the operator for one.
|
|
18
|
+
|
|
19
|
+
## Output
|
|
20
|
+
|
|
21
|
+
A directory under `~/Documents/Maxy/preval/<postcode-compact>-<YYYY-MM-DD>/` containing:
|
|
22
|
+
|
|
23
|
+
- `index.html` — single self-contained file.
|
|
24
|
+
- `<slug>-<date>.pdf` — exactly 4 A4 pages. If `mdls -name kMDItemNumberOfPages` returns ≠ 4, the renderer raises rather than ship an overflowing report.
|
|
25
|
+
|
|
26
|
+
Return the PDF path to the operator.
|
|
27
|
+
|
|
28
|
+
## Tools the skill calls (in order)
|
|
29
|
+
|
|
30
|
+
Fan-out is fixed at **12** `real-agent-property-data` MCP tools, keyed on the derived postcode (and address attributes where the tool needs them):
|
|
31
|
+
|
|
32
|
+
1. `property-data-prices` — asking £ distribution + raw asking comps.
|
|
33
|
+
2. `property-data-prices-per-sqf` — asking £/sqft.
|
|
34
|
+
3. `property-data-sold-prices` — Land Registry sold transactions + raw sold comps.
|
|
35
|
+
4. `property-data-sold-prices-per-sqf` — achieved £/sqft.
|
|
36
|
+
5. `property-data-growth` — 7y price series.
|
|
37
|
+
6. `property-data-growth-psf` — 7y £/sqft series.
|
|
38
|
+
7. `property-data-demand` — for-sale, sales/mo, turnover, MoI, DoM, demand rating.
|
|
39
|
+
8. `property-data-demand-rent` — rental-side demand pillar.
|
|
40
|
+
9. `property-data-property-types` — mix doughnut (detached / semi / terraced / flat).
|
|
41
|
+
10. `property-data-crime` — area crime rating + breakdown.
|
|
42
|
+
11. `property-data-flood-risk` — area flood risk (full postcode only).
|
|
43
|
+
12. `property-data-council-tax` — band D cost + per-property bands (full postcode only).
|
|
44
|
+
|
|
45
|
+
## Tools the skill deliberately does NOT call
|
|
46
|
+
|
|
47
|
+
- `property-data-address-match-uprn` — UPRN is CRM hygiene, not preval data.
|
|
48
|
+
- `property-data-planning-applications` — planning is a separate dev-precedent report.
|
|
49
|
+
|
|
50
|
+
Both remain available via the underlying `property-data` skill; preval omits them to keep the output focused and the credit cost at 12 per pack.
|
|
51
|
+
|
|
52
|
+
## How to execute
|
|
53
|
+
|
|
54
|
+
1. Match the postcode out of the address (`/[A-Z]{1,2}[0-9][A-Z0-9]?\s?[0-9][A-Z]{2}/i`). Stop and ask if no match.
|
|
55
|
+
2. Run the 12 fan-out tools in the order above. Any `Error: key-not-registered` aborts the run — surface the registration prompt and stop.
|
|
56
|
+
3. Assemble the `inputs.json` shape per `references/template-inputs.schema.json`.
|
|
57
|
+
4. Run `python3 references/render.py inputs.json`. The renderer fills the template, writes the HTML, drives Chrome `--print-to-pdf`, and asserts page count = 4.
|
|
58
|
+
5. Return the PDF path.
|
|
59
|
+
|
|
60
|
+
## Observability
|
|
61
|
+
|
|
62
|
+
Emit at start: `[preval] start address="<addr>" postcode=<pc>`.
|
|
63
|
+
Emit at done: `[preval] done pdf=<path> pages=<n> ms=<n>`.
|
|
64
|
+
|
|
65
|
+
Each of the 12 fan-out calls produces its own `[property-data] tool=<name>` line in `server.log` (inherited contract — no new instrumentation).
|
|
66
|
+
|
|
67
|
+
## Failure signatures
|
|
68
|
+
|
|
69
|
+
- `pages=5` (or anything ≠ 4) in the `done` line → template overflow on one page. Diagnose with `pdftoppm -png -f 5 -l 5 <pdf>` to see which content bled, then trim that panel's padding.
|
|
70
|
+
- `start` with no matching `done` within 60s → renderer crashed. Inspect with `grep -B2 -A20 '\[preval\] start address="<echoed>"' server.log`.
|
|
71
|
+
- Wrong postcode derivation → check `grep '\[preval\] start' server.log | awk -F'postcode=' '{print $2}' | sort -u`; the regex must keep the inward part of the postcode (e.g. `CM23 2UJ`, not `CM23`).
|
|
72
|
+
|
|
73
|
+
## Out of scope
|
|
74
|
+
|
|
75
|
+
- Multi-address batch preval (CSV in → many PDFs out) — separate skill if needed.
|
|
76
|
+
- Loop CRM auto-publish of the PDF onto a contact's record — separate skill.
|
|
77
|
+
- A hero image on the cover via `property-socials` — preval cover is text-only by design.
|
|
78
|
+
- Caching of MCP responses — 12 credits per pack is acceptable; cost optimisation is a Task 144B concern.
|