estatehelm 1.0.6 → 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 "***";
@@ -1547,11 +1583,11 @@ function waitForCallback(port) {
1547
1583
  const sessionToken = url.searchParams.get("session_token");
1548
1584
  const error = url.searchParams.get("error");
1549
1585
  if (error) {
1550
- res.writeHead(400, { "Content-Type": "text/html" });
1586
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
1551
1587
  res.end(`
1552
1588
  <!DOCTYPE html>
1553
1589
  <html>
1554
- <head><title>Authentication Failed</title>
1590
+ <head><meta charset="utf-8"><title>Authentication Failed</title>
1555
1591
  <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#fef2f2}.card{background:white;padding:2rem;border-radius:1rem;box-shadow:0 10px 25px rgba(0,0,0,0.1);text-align:center}h1{color:#dc2626}</style></head>
1556
1592
  <body><div class="card"><h1>Authentication Failed</h1><p>${url.searchParams.get("error_description") || error}</p></div></body>
1557
1593
  </html>
@@ -1561,13 +1597,13 @@ function waitForCallback(port) {
1561
1597
  return;
1562
1598
  }
1563
1599
  if (sessionToken) {
1564
- res.writeHead(200, { "Content-Type": "text/html" });
1600
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1565
1601
  res.end(`
1566
1602
  <!DOCTYPE html>
1567
1603
  <html>
1568
- <head><title>Authentication Successful</title>
1604
+ <head><meta charset="utf-8"><title>Authentication Successful</title>
1569
1605
  <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:linear-gradient(115deg,#fff1be 28%,#ee87cb 70%,#b060ff 100%)}.card{background:white;padding:2rem;border-radius:1rem;box-shadow:0 10px 25px rgba(0,0,0,0.1);text-align:center}h1{color:#059669}</style></head>
1570
- <body><div class="card"><h1>\u2713 Authentication Successful</h1><p>You can close this window and return to your terminal.</p></div></body>
1606
+ <body><div class="card"><h1>&#10003; Authentication Successful</h1><p>You can close this window and return to your terminal.</p></div></body>
1571
1607
  </html>
1572
1608
  `);
1573
1609
  server.close();
@@ -1628,7 +1664,15 @@ ${loginUrl}
1628
1664
  privateKeyBytes: base64Encode(privateKeyBytes)
1629
1665
  });
1630
1666
  console.log("\n\u2713 Login complete!");
1631
- console.log("You can now use: estatehelm mcp");
1667
+ console.log("\nTo use with Claude Code, run:");
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");
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
+ }
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");
1675
+ console.log("\nOr run manually: npx estatehelm mcp");
1632
1676
  }
1633
1677
  async function checkLogin() {
1634
1678
  const credentials = await getCredentials();
@@ -1748,10 +1792,16 @@ var SqliteCacheStore = class {
1748
1792
  this.initializeSchema();
1749
1793
  }
1750
1794
  initializeSchema() {
1751
- const versionRow = this.db.prepare(
1752
- "SELECT value FROM metadata WHERE key = 'schema_version'"
1795
+ const tableExists = this.db.prepare(
1796
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='metadata'"
1753
1797
  ).get();
1754
- const currentVersion = versionRow ? parseInt(versionRow.value, 10) : 0;
1798
+ let currentVersion = 0;
1799
+ if (tableExists) {
1800
+ const versionRow = this.db.prepare(
1801
+ "SELECT value FROM metadata WHERE key = 'schema_version'"
1802
+ ).get();
1803
+ currentVersion = versionRow ? parseInt(versionRow.value, 10) : 0;
1804
+ }
1755
1805
  if (currentVersion < DB_VERSION) {
1756
1806
  this.migrate(currentVersion);
1757
1807
  }
@@ -2400,6 +2450,40 @@ async function getCacheStats() {
2400
2450
  const cache = getCache();
2401
2451
  return cache.getCacheStats();
2402
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
+ }
2403
2487
 
2404
2488
  // src/server.ts
2405
2489
  async function startServer(mode) {
@@ -2577,6 +2661,32 @@ async function startServer(mode) {
2577
2661
  type: "object",
2578
2662
  properties: {}
2579
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
+ }
2580
2690
  }
2581
2691
  ]
2582
2692
  };
@@ -2677,15 +2787,15 @@ async function startServer(mode) {
2677
2787
  for (const household of searchHouseholds) {
2678
2788
  const insurance = await getDecryptedEntities(household.id, "insurance", privateKey);
2679
2789
  for (const policy of insurance) {
2680
- if (policy.expirationDate) {
2681
- const expires = new Date(policy.expirationDate);
2790
+ if (policy.expiration_date) {
2791
+ const expires = new Date(policy.expiration_date);
2682
2792
  if (expires <= cutoff) {
2683
2793
  expiring.push({
2684
2794
  householdId: household.id,
2685
2795
  householdName: household.name,
2686
2796
  type: "insurance",
2687
- name: policy.name || policy.policyNumber,
2688
- expiresAt: policy.expirationDate,
2797
+ name: policy.name || policy.policy_number,
2798
+ expiresAt: policy.expiration_date,
2689
2799
  daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
2690
2800
  });
2691
2801
  }
@@ -2693,15 +2803,29 @@ async function startServer(mode) {
2693
2803
  }
2694
2804
  const vehicles = await getDecryptedEntities(household.id, "vehicle", privateKey);
2695
2805
  for (const vehicle of vehicles) {
2696
- if (vehicle.registrationExpiration) {
2697
- 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);
2698
2809
  if (expires <= cutoff) {
2699
2810
  expiring.push({
2700
2811
  householdId: household.id,
2701
2812
  householdName: household.name,
2702
2813
  type: "vehicle_registration",
2703
- name: `${vehicle.year || ""} ${vehicle.make || ""} ${vehicle.model || ""}`.trim(),
2704
- 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,
2705
2829
  daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
2706
2830
  });
2707
2831
  }
@@ -2709,16 +2833,16 @@ async function startServer(mode) {
2709
2833
  }
2710
2834
  const subscriptions = await getDecryptedEntities(household.id, "subscription", privateKey);
2711
2835
  for (const sub of subscriptions) {
2712
- if (sub.renewalDate) {
2713
- const renews = new Date(sub.renewalDate);
2714
- if (renews <= cutoff) {
2836
+ if (sub.expiration_date) {
2837
+ const expires = new Date(sub.expiration_date);
2838
+ if (expires <= cutoff) {
2715
2839
  expiring.push({
2716
2840
  householdId: household.id,
2717
2841
  householdName: household.name,
2718
2842
  type: "subscription",
2719
- name: sub.name || sub.serviceName,
2720
- expiresAt: sub.renewalDate,
2721
- 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))
2722
2846
  });
2723
2847
  }
2724
2848
  }
@@ -2745,6 +2869,44 @@ async function startServer(mode) {
2745
2869
  ]
2746
2870
  };
2747
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
+ }
2748
2910
  default:
2749
2911
  throw new Error(`Unknown tool: ${name}`);
2750
2912
  }