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 CHANGED
@@ -24,19 +24,27 @@ This will:
24
24
 
25
25
  ### 2. Configure Claude Code
26
26
 
27
- Add to your `~/.claude.json` (or Claude Code settings):
28
-
29
- ```json
30
- {
31
- "mcpServers": {
32
- "estatehelm": {
33
- "command": "estatehelm",
34
- "args": ["mcp"]
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}/pets` - Pets in household
126
- - `estatehelm://households/{id}/properties` - Properties
127
- - `estatehelm://households/{id}/vehicles` - Vehicles
128
- - `estatehelm://households/{id}/contacts` - Contacts
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
- - `search_entities` - Full-text search across entities
136
- - `get_household_summary` - Overview with counts
137
- - `get_expiring_items` - Items expiring soon
138
- - `refresh` - Force re-sync from server
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
- return "macOS";
1404
+ osName = "macOS";
1405
+ break;
1373
1406
  case "win32":
1374
- return "Windows";
1407
+ osName = "Windows";
1408
+ break;
1375
1409
  case "linux":
1376
- return "Linux";
1410
+ osName = "Linux";
1411
+ break;
1377
1412
  default:
1378
- return platform;
1413
+ osName = platform;
1379
1414
  }
1415
+ return `mcp-${osName}-${hostname2}`;
1380
1416
  }
1381
1417
  function getDeviceUserAgent() {
1382
- return `estatehelm-mcp/1.0 (${getDevicePlatform()})`;
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, add to your MCP settings:");
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
- console.log(JSON.stringify({
1634
- mcpServers: {
1635
- estatehelm: {
1636
- command: "npx",
1637
- args: ["estatehelm", "mcp"]
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.expirationDate) {
2698
- const expires = new Date(policy.expirationDate);
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.policyNumber,
2705
- expiresAt: policy.expirationDate,
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
- if (vehicle.registrationExpiration) {
2714
- const expires = new Date(vehicle.registrationExpiration);
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: `${vehicle.year || ""} ${vehicle.make || ""} ${vehicle.model || ""}`.trim(),
2721
- expiresAt: vehicle.registrationExpiration,
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.renewalDate) {
2730
- const renews = new Date(sub.renewalDate);
2731
- if (renews <= cutoff) {
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.serviceName,
2737
- expiresAt: sub.renewalDate,
2738
- daysUntil: Math.ceil((renews.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
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:\/\/([^/]+)(?:\/([^/]+))?(?:\/([^/]+))?(?:\/([^/]+))?$/);