estatehelm 1.0.7 → 1.0.8
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/README.md +50 -22
- package/dist/index.js +174 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,19 +24,27 @@ This will:
|
|
|
24
24
|
|
|
25
25
|
### 2. Configure Claude Code
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
**Windows:**
|
|
28
|
+
```bash
|
|
29
|
+
claude mcp add --transport stdio estatehelm -- cmd /c npx estatehelm mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Mac/Linux:**
|
|
33
|
+
```bash
|
|
34
|
+
claude mcp add --transport stdio estatehelm -- npx estatehelm mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**For staging environment:**
|
|
38
|
+
```bash
|
|
39
|
+
# Windows
|
|
40
|
+
claude mcp add --transport stdio estatehelm-staging -- cmd /c npx estatehelm mcp --staging
|
|
41
|
+
|
|
42
|
+
# Mac/Linux
|
|
43
|
+
claude mcp add --transport stdio estatehelm-staging -- npx estatehelm mcp --staging
|
|
38
44
|
```
|
|
39
45
|
|
|
46
|
+
Verify with `claude mcp list`.
|
|
47
|
+
|
|
40
48
|
### 3. Use with Claude Code
|
|
41
49
|
|
|
42
50
|
Start Claude Code and you can now ask questions like:
|
|
@@ -122,20 +130,40 @@ The server exposes your EstateHelm data as MCP resources:
|
|
|
122
130
|
|
|
123
131
|
- `estatehelm://households` - List all households
|
|
124
132
|
- `estatehelm://households/{id}` - Specific household
|
|
125
|
-
- `estatehelm://households/{id}/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- `estatehelm://households/{id}/insurance` - Insurance policies
|
|
130
|
-
- `estatehelm://households/{id}/bank_account` - Bank accounts
|
|
131
|
-
- And more...
|
|
133
|
+
- `estatehelm://households/{id}/{entityType}` - Entities by type
|
|
134
|
+
|
|
135
|
+
**Supported entity types:**
|
|
136
|
+
`pet`, `property`, `vehicle`, `contact`, `insurance`, `bank_account`, `investment`, `subscription`, `maintenance_task`, `password`, `access_code`, `document`, `medical`, `prescription`, `credential`, `utility`
|
|
132
137
|
|
|
133
138
|
## MCP Tools
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
| Tool | Description | Parameters |
|
|
141
|
+
|------|-------------|------------|
|
|
142
|
+
| `search_entities` | Full-text search across entities | `query` (required), `householdId`, `entityType` |
|
|
143
|
+
| `get_household_summary` | Overview with entity counts | `householdId` (required) |
|
|
144
|
+
| `get_expiring_items` | Items expiring within N days | `days` (default: 30), `householdId` |
|
|
145
|
+
| `get_file` | Download and decrypt a file attachment | `fileId`, `householdId`, `entityId`, `entityType` (all required) |
|
|
146
|
+
| `refresh` | Force re-sync from server | none |
|
|
147
|
+
|
|
148
|
+
**`get_expiring_items` checks:**
|
|
149
|
+
- Insurance policies (`expiration_date`)
|
|
150
|
+
- Vehicle registrations (`registration_expiration`)
|
|
151
|
+
- Vehicle tabs (`tabs_expiration`)
|
|
152
|
+
- Subscriptions (`expiration_date`)
|
|
153
|
+
|
|
154
|
+
**`get_file` returns:**
|
|
155
|
+
- `fileName`: Original filename
|
|
156
|
+
- `mimeType`: File MIME type
|
|
157
|
+
- `size`: File size in bytes
|
|
158
|
+
- `data`: Base64-encoded decrypted file content
|
|
159
|
+
|
|
160
|
+
## MCP Prompts
|
|
161
|
+
|
|
162
|
+
| Prompt | Description |
|
|
163
|
+
|--------|-------------|
|
|
164
|
+
| `household_summary` | "Give me an overview of my household" |
|
|
165
|
+
| `expiring_soon` | "What's expiring in the next 30 days?" |
|
|
166
|
+
| `emergency_contacts` | "Show me emergency contacts" |
|
|
139
167
|
|
|
140
168
|
## Security
|
|
141
169
|
|
package/dist/index.js
CHANGED
|
@@ -1297,10 +1297,40 @@ async function unwrapHouseholdKey(wrappedKey, privateKey, algorithm = DEFAULT_KE
|
|
|
1297
1297
|
// ../encryption/src/webauthnDeviceBound.ts
|
|
1298
1298
|
var RP_ID = typeof window !== "undefined" ? window.location.hostname : "estatehelm.com";
|
|
1299
1299
|
|
|
1300
|
+
// ../encryption/src/fileEncryption.ts
|
|
1301
|
+
var FILE_IV_SIZE = 12;
|
|
1302
|
+
async function decryptFileFromArrayBuffer(householdKey, entityId, entityType, encryptedArrayBuffer, mimeType) {
|
|
1303
|
+
const packed = new Uint8Array(encryptedArrayBuffer);
|
|
1304
|
+
if (packed.length < FILE_IV_SIZE + 16) {
|
|
1305
|
+
throw new Error("Encrypted data too short");
|
|
1306
|
+
}
|
|
1307
|
+
const entityKeyBytes = await deriveEntityKey(householdKey, entityId, entityType);
|
|
1308
|
+
const entityKey = await importEntityKey(entityKeyBytes);
|
|
1309
|
+
const iv = packed.slice(0, FILE_IV_SIZE);
|
|
1310
|
+
const ciphertext = packed.slice(FILE_IV_SIZE);
|
|
1311
|
+
try {
|
|
1312
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
1313
|
+
{ name: "AES-GCM", iv },
|
|
1314
|
+
entityKey,
|
|
1315
|
+
ciphertext
|
|
1316
|
+
);
|
|
1317
|
+
return {
|
|
1318
|
+
bytes: new Uint8Array(plaintext),
|
|
1319
|
+
mimeType
|
|
1320
|
+
};
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
if (error instanceof Error && error.name === "OperationError") {
|
|
1323
|
+
throw new Error("Failed to decrypt file: Authentication failed");
|
|
1324
|
+
}
|
|
1325
|
+
throw error;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1300
1329
|
// src/config.ts
|
|
1301
1330
|
var import_env_paths = __toESM(require("env-paths"));
|
|
1302
1331
|
var path = __toESM(require("path"));
|
|
1303
1332
|
var fs = __toESM(require("fs"));
|
|
1333
|
+
var os = __toESM(require("os"));
|
|
1304
1334
|
var paths = (0, import_env_paths.default)("estatehelm", { suffix: "" });
|
|
1305
1335
|
var DATA_DIR = paths.data;
|
|
1306
1336
|
var CACHE_DB_PATH = path.join(DATA_DIR, "cache.db");
|
|
@@ -1367,19 +1397,25 @@ function getDeviceId() {
|
|
|
1367
1397
|
}
|
|
1368
1398
|
function getDevicePlatform() {
|
|
1369
1399
|
const platform = process.platform;
|
|
1400
|
+
const hostname2 = os.hostname();
|
|
1401
|
+
let osName;
|
|
1370
1402
|
switch (platform) {
|
|
1371
1403
|
case "darwin":
|
|
1372
|
-
|
|
1404
|
+
osName = "macOS";
|
|
1405
|
+
break;
|
|
1373
1406
|
case "win32":
|
|
1374
|
-
|
|
1407
|
+
osName = "Windows";
|
|
1408
|
+
break;
|
|
1375
1409
|
case "linux":
|
|
1376
|
-
|
|
1410
|
+
osName = "Linux";
|
|
1411
|
+
break;
|
|
1377
1412
|
default:
|
|
1378
|
-
|
|
1413
|
+
osName = platform;
|
|
1379
1414
|
}
|
|
1415
|
+
return `mcp-${osName}-${hostname2}`;
|
|
1380
1416
|
}
|
|
1381
1417
|
function getDeviceUserAgent() {
|
|
1382
|
-
return `estatehelm-mcp/1.0 (${
|
|
1418
|
+
return `estatehelm-mcp/1.0 (${os.hostname()}, ${process.platform})`;
|
|
1383
1419
|
}
|
|
1384
1420
|
function sanitizeToken(token) {
|
|
1385
1421
|
if (token.length <= 8) return "***";
|
|
@@ -1628,18 +1664,15 @@ ${loginUrl}
|
|
|
1628
1664
|
privateKeyBytes: base64Encode(privateKeyBytes)
|
|
1629
1665
|
});
|
|
1630
1666
|
console.log("\n\u2713 Login complete!");
|
|
1631
|
-
console.log("\nTo use with Claude Code,
|
|
1667
|
+
console.log("\nTo use with Claude Code, run:");
|
|
1632
1668
|
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
}, null, 2));
|
|
1669
|
+
if (process.platform === "win32") {
|
|
1670
|
+
console.log(" claude mcp add --transport stdio estatehelm -- cmd /c npx estatehelm mcp");
|
|
1671
|
+
} else {
|
|
1672
|
+
console.log(" claude mcp add --transport stdio estatehelm -- npx estatehelm mcp");
|
|
1673
|
+
}
|
|
1641
1674
|
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1642
|
-
console.log("\nOr run manually: estatehelm mcp");
|
|
1675
|
+
console.log("\nOr run manually: npx estatehelm mcp");
|
|
1643
1676
|
}
|
|
1644
1677
|
async function checkLogin() {
|
|
1645
1678
|
const credentials = await getCredentials();
|
|
@@ -2417,6 +2450,40 @@ async function getCacheStats() {
|
|
|
2417
2450
|
const cache = getCache();
|
|
2418
2451
|
return cache.getCacheStats();
|
|
2419
2452
|
}
|
|
2453
|
+
async function downloadAndDecryptFile(client, householdId, fileId, entityId, entityType) {
|
|
2454
|
+
const keyType = getKeyTypeForEntity(entityType);
|
|
2455
|
+
const householdKeyBytes = householdKeysCache.get(`${householdId}:${keyType}`);
|
|
2456
|
+
if (!householdKeyBytes) {
|
|
2457
|
+
throw new Error(`No key available for ${householdId}:${keyType}`);
|
|
2458
|
+
}
|
|
2459
|
+
const fileInfo = await client.get(
|
|
2460
|
+
`/households/${householdId}/files/${fileId}`
|
|
2461
|
+
);
|
|
2462
|
+
if (!fileInfo.downloadUrl) {
|
|
2463
|
+
throw new Error("No download URL returned for file");
|
|
2464
|
+
}
|
|
2465
|
+
const response = await fetch(fileInfo.downloadUrl);
|
|
2466
|
+
if (!response.ok) {
|
|
2467
|
+
throw new Error(`Failed to download file: ${response.status}`);
|
|
2468
|
+
}
|
|
2469
|
+
const encryptedBytes = await response.arrayBuffer();
|
|
2470
|
+
const cryptoVersion = fileInfo.cryptoVersion ?? 1;
|
|
2471
|
+
const keyDerivationId = cryptoVersion === 2 ? fileId : fileInfo.entityId || entityId;
|
|
2472
|
+
const mimeType = fileInfo.fileType || "application/octet-stream";
|
|
2473
|
+
const decrypted = await decryptFileFromArrayBuffer(
|
|
2474
|
+
householdKeyBytes,
|
|
2475
|
+
keyDerivationId,
|
|
2476
|
+
entityType,
|
|
2477
|
+
encryptedBytes,
|
|
2478
|
+
mimeType
|
|
2479
|
+
);
|
|
2480
|
+
return {
|
|
2481
|
+
data: decrypted.data,
|
|
2482
|
+
dataBase64: base64Encode(decrypted.data),
|
|
2483
|
+
mimeType: decrypted.mimeType,
|
|
2484
|
+
fileName: fileInfo.fileName
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2420
2487
|
|
|
2421
2488
|
// src/server.ts
|
|
2422
2489
|
async function startServer(mode) {
|
|
@@ -2594,6 +2661,32 @@ async function startServer(mode) {
|
|
|
2594
2661
|
type: "object",
|
|
2595
2662
|
properties: {}
|
|
2596
2663
|
}
|
|
2664
|
+
},
|
|
2665
|
+
{
|
|
2666
|
+
name: "get_file",
|
|
2667
|
+
description: "Download and decrypt a file attachment. Returns base64-encoded file data that can be saved to disk.",
|
|
2668
|
+
inputSchema: {
|
|
2669
|
+
type: "object",
|
|
2670
|
+
properties: {
|
|
2671
|
+
fileId: {
|
|
2672
|
+
type: "string",
|
|
2673
|
+
description: "The file ID to download"
|
|
2674
|
+
},
|
|
2675
|
+
householdId: {
|
|
2676
|
+
type: "string",
|
|
2677
|
+
description: "The household ID the file belongs to"
|
|
2678
|
+
},
|
|
2679
|
+
entityId: {
|
|
2680
|
+
type: "string",
|
|
2681
|
+
description: "The entity ID the file is attached to"
|
|
2682
|
+
},
|
|
2683
|
+
entityType: {
|
|
2684
|
+
type: "string",
|
|
2685
|
+
description: "The entity type (e.g., insurance, document)"
|
|
2686
|
+
}
|
|
2687
|
+
},
|
|
2688
|
+
required: ["fileId", "householdId", "entityId", "entityType"]
|
|
2689
|
+
}
|
|
2597
2690
|
}
|
|
2598
2691
|
]
|
|
2599
2692
|
};
|
|
@@ -2694,15 +2787,15 @@ async function startServer(mode) {
|
|
|
2694
2787
|
for (const household of searchHouseholds) {
|
|
2695
2788
|
const insurance = await getDecryptedEntities(household.id, "insurance", privateKey);
|
|
2696
2789
|
for (const policy of insurance) {
|
|
2697
|
-
if (policy.
|
|
2698
|
-
const expires = new Date(policy.
|
|
2790
|
+
if (policy.expiration_date) {
|
|
2791
|
+
const expires = new Date(policy.expiration_date);
|
|
2699
2792
|
if (expires <= cutoff) {
|
|
2700
2793
|
expiring.push({
|
|
2701
2794
|
householdId: household.id,
|
|
2702
2795
|
householdName: household.name,
|
|
2703
2796
|
type: "insurance",
|
|
2704
|
-
name: policy.name || policy.
|
|
2705
|
-
expiresAt: policy.
|
|
2797
|
+
name: policy.name || policy.policy_number,
|
|
2798
|
+
expiresAt: policy.expiration_date,
|
|
2706
2799
|
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
2707
2800
|
});
|
|
2708
2801
|
}
|
|
@@ -2710,15 +2803,29 @@ async function startServer(mode) {
|
|
|
2710
2803
|
}
|
|
2711
2804
|
const vehicles = await getDecryptedEntities(household.id, "vehicle", privateKey);
|
|
2712
2805
|
for (const vehicle of vehicles) {
|
|
2713
|
-
|
|
2714
|
-
|
|
2806
|
+
const vehicleName = `${vehicle.year || ""} ${vehicle.make || ""} ${vehicle.model || ""}`.trim();
|
|
2807
|
+
if (vehicle.registration_expiration) {
|
|
2808
|
+
const expires = new Date(vehicle.registration_expiration);
|
|
2715
2809
|
if (expires <= cutoff) {
|
|
2716
2810
|
expiring.push({
|
|
2717
2811
|
householdId: household.id,
|
|
2718
2812
|
householdName: household.name,
|
|
2719
2813
|
type: "vehicle_registration",
|
|
2720
|
-
name:
|
|
2721
|
-
expiresAt: vehicle.
|
|
2814
|
+
name: vehicleName,
|
|
2815
|
+
expiresAt: vehicle.registration_expiration,
|
|
2816
|
+
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
if (vehicle.tabs_expiration) {
|
|
2821
|
+
const expires = new Date(vehicle.tabs_expiration);
|
|
2822
|
+
if (expires <= cutoff) {
|
|
2823
|
+
expiring.push({
|
|
2824
|
+
householdId: household.id,
|
|
2825
|
+
householdName: household.name,
|
|
2826
|
+
type: "vehicle_tabs",
|
|
2827
|
+
name: vehicleName,
|
|
2828
|
+
expiresAt: vehicle.tabs_expiration,
|
|
2722
2829
|
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
2723
2830
|
});
|
|
2724
2831
|
}
|
|
@@ -2726,16 +2833,16 @@ async function startServer(mode) {
|
|
|
2726
2833
|
}
|
|
2727
2834
|
const subscriptions = await getDecryptedEntities(household.id, "subscription", privateKey);
|
|
2728
2835
|
for (const sub of subscriptions) {
|
|
2729
|
-
if (sub.
|
|
2730
|
-
const
|
|
2731
|
-
if (
|
|
2836
|
+
if (sub.expiration_date) {
|
|
2837
|
+
const expires = new Date(sub.expiration_date);
|
|
2838
|
+
if (expires <= cutoff) {
|
|
2732
2839
|
expiring.push({
|
|
2733
2840
|
householdId: household.id,
|
|
2734
2841
|
householdName: household.name,
|
|
2735
2842
|
type: "subscription",
|
|
2736
|
-
name: sub.name || sub.
|
|
2737
|
-
expiresAt: sub.
|
|
2738
|
-
daysUntil: Math.ceil((
|
|
2843
|
+
name: sub.name || sub.service_name,
|
|
2844
|
+
expiresAt: sub.expiration_date,
|
|
2845
|
+
daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
|
|
2739
2846
|
});
|
|
2740
2847
|
}
|
|
2741
2848
|
}
|
|
@@ -2762,6 +2869,44 @@ async function startServer(mode) {
|
|
|
2762
2869
|
]
|
|
2763
2870
|
};
|
|
2764
2871
|
}
|
|
2872
|
+
case "get_file": {
|
|
2873
|
+
const { fileId, householdId, entityId, entityType } = args;
|
|
2874
|
+
try {
|
|
2875
|
+
const result = await downloadAndDecryptFile(
|
|
2876
|
+
client,
|
|
2877
|
+
householdId,
|
|
2878
|
+
fileId,
|
|
2879
|
+
entityId,
|
|
2880
|
+
entityType
|
|
2881
|
+
);
|
|
2882
|
+
return {
|
|
2883
|
+
content: [
|
|
2884
|
+
{
|
|
2885
|
+
type: "text",
|
|
2886
|
+
text: JSON.stringify({
|
|
2887
|
+
success: true,
|
|
2888
|
+
fileName: result.fileName,
|
|
2889
|
+
mimeType: result.mimeType,
|
|
2890
|
+
size: result.data.length,
|
|
2891
|
+
data: result.dataBase64
|
|
2892
|
+
}, null, 2)
|
|
2893
|
+
}
|
|
2894
|
+
]
|
|
2895
|
+
};
|
|
2896
|
+
} catch (err) {
|
|
2897
|
+
return {
|
|
2898
|
+
content: [
|
|
2899
|
+
{
|
|
2900
|
+
type: "text",
|
|
2901
|
+
text: JSON.stringify({
|
|
2902
|
+
success: false,
|
|
2903
|
+
error: err.message
|
|
2904
|
+
}, null, 2)
|
|
2905
|
+
}
|
|
2906
|
+
]
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2765
2910
|
default:
|
|
2766
2911
|
throw new Error(`Unknown tool: ${name}`);
|
|
2767
2912
|
}
|
|
@@ -2855,18 +3000,6 @@ async function startServer(mode) {
|
|
|
2855
3000
|
const transport = new import_stdio.StdioServerTransport();
|
|
2856
3001
|
await server.connect(transport);
|
|
2857
3002
|
console.error("[MCP] Server started");
|
|
2858
|
-
console.error("");
|
|
2859
|
-
console.error("To use with Claude Code, add to ~/.claude/claude_desktop_config.json:");
|
|
2860
|
-
console.error("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2861
|
-
console.error(JSON.stringify({
|
|
2862
|
-
mcpServers: {
|
|
2863
|
-
estatehelm: {
|
|
2864
|
-
command: "npx",
|
|
2865
|
-
args: ["estatehelm", "mcp"]
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
}, null, 2));
|
|
2869
|
-
console.error("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2870
3003
|
}
|
|
2871
3004
|
function parseResourceUri(uri) {
|
|
2872
3005
|
const match = uri.match(/^estatehelm:\/\/([^/]+)(?:\/([^/]+))?(?:\/([^/]+))?(?:\/([^/]+))?$/);
|