estatehelm 1.0.7 → 1.0.9

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/dist/index.js CHANGED
@@ -933,6 +933,16 @@ async function deriveWrapKey(recoveryKeyBytes, serverWrapSecret, info = "hearthc
933
933
  return wrapKey;
934
934
  }
935
935
 
936
+ // ../types/src/contacts.ts
937
+ function getContactDisplayName(contact, fallback = "") {
938
+ const personName = [contact.first_name, contact.last_name].filter(Boolean).join(" ");
939
+ const companyName = contact.company_name || "";
940
+ if (personName && companyName) {
941
+ return `${personName} / ${companyName}`;
942
+ }
943
+ return personName || companyName || fallback;
944
+ }
945
+
936
946
  // ../types/src/keys.ts
937
947
  var DEFAULT_KEY_BUNDLE_ALG = "ECDH-P-521";
938
948
 
@@ -1297,10 +1307,40 @@ async function unwrapHouseholdKey(wrappedKey, privateKey, algorithm = DEFAULT_KE
1297
1307
  // ../encryption/src/webauthnDeviceBound.ts
1298
1308
  var RP_ID = typeof window !== "undefined" ? window.location.hostname : "estatehelm.com";
1299
1309
 
1310
+ // ../encryption/src/fileEncryption.ts
1311
+ var FILE_IV_SIZE = 12;
1312
+ async function decryptFileFromArrayBuffer(householdKey, entityId, entityType, encryptedArrayBuffer, mimeType) {
1313
+ const packed = new Uint8Array(encryptedArrayBuffer);
1314
+ if (packed.length < FILE_IV_SIZE + 16) {
1315
+ throw new Error("Encrypted data too short");
1316
+ }
1317
+ const entityKeyBytes = await deriveEntityKey(householdKey, entityId, entityType);
1318
+ const entityKey = await importEntityKey(entityKeyBytes);
1319
+ const iv = packed.slice(0, FILE_IV_SIZE);
1320
+ const ciphertext = packed.slice(FILE_IV_SIZE);
1321
+ try {
1322
+ const plaintext = await crypto.subtle.decrypt(
1323
+ { name: "AES-GCM", iv },
1324
+ entityKey,
1325
+ ciphertext
1326
+ );
1327
+ return {
1328
+ bytes: new Uint8Array(plaintext),
1329
+ mimeType
1330
+ };
1331
+ } catch (error) {
1332
+ if (error instanceof Error && error.name === "OperationError") {
1333
+ throw new Error("Failed to decrypt file: Authentication failed");
1334
+ }
1335
+ throw error;
1336
+ }
1337
+ }
1338
+
1300
1339
  // src/config.ts
1301
1340
  var import_env_paths = __toESM(require("env-paths"));
1302
1341
  var path = __toESM(require("path"));
1303
1342
  var fs = __toESM(require("fs"));
1343
+ var os = __toESM(require("os"));
1304
1344
  var paths = (0, import_env_paths.default)("estatehelm", { suffix: "" });
1305
1345
  var DATA_DIR = paths.data;
1306
1346
  var CACHE_DB_PATH = path.join(DATA_DIR, "cache.db");
@@ -1367,19 +1407,25 @@ function getDeviceId() {
1367
1407
  }
1368
1408
  function getDevicePlatform() {
1369
1409
  const platform = process.platform;
1410
+ const hostname2 = os.hostname();
1411
+ let osName;
1370
1412
  switch (platform) {
1371
1413
  case "darwin":
1372
- return "macOS";
1414
+ osName = "macOS";
1415
+ break;
1373
1416
  case "win32":
1374
- return "Windows";
1417
+ osName = "Windows";
1418
+ break;
1375
1419
  case "linux":
1376
- return "Linux";
1420
+ osName = "Linux";
1421
+ break;
1377
1422
  default:
1378
- return platform;
1423
+ osName = platform;
1379
1424
  }
1425
+ return `mcp-${osName}-${hostname2}`;
1380
1426
  }
1381
1427
  function getDeviceUserAgent() {
1382
- return `estatehelm-mcp/1.0 (${getDevicePlatform()})`;
1428
+ return `estatehelm-mcp/1.0 (${os.hostname()}, ${process.platform})`;
1383
1429
  }
1384
1430
  function sanitizeToken(token) {
1385
1431
  if (token.length <= 8) return "***";
@@ -1628,18 +1674,15 @@ ${loginUrl}
1628
1674
  privateKeyBytes: base64Encode(privateKeyBytes)
1629
1675
  });
1630
1676
  console.log("\n\u2713 Login complete!");
1631
- console.log("\nTo use with Claude Code, add to your MCP settings:");
1677
+ console.log("\nTo use with Claude Code, run:");
1632
1678
  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));
1679
+ if (process.platform === "win32") {
1680
+ console.log(" claude mcp add --transport stdio estatehelm -- cmd /c npx estatehelm mcp");
1681
+ } else {
1682
+ console.log(" claude mcp add --transport stdio estatehelm -- npx estatehelm mcp");
1683
+ }
1641
1684
  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");
1685
+ console.log("\nOr run manually: npx estatehelm mcp");
1643
1686
  }
1644
1687
  async function checkLogin() {
1645
1688
  const credentials = await getCredentials();
@@ -1692,45 +1735,7 @@ async function getPrivateKey() {
1692
1735
  // src/server.ts
1693
1736
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
1694
1737
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
1695
- var import_types5 = require("@modelcontextprotocol/sdk/types.js");
1696
-
1697
- // src/filter.ts
1698
- var REDACTION_RULES = {
1699
- // Password entries
1700
- password: ["password", "notes"],
1701
- // Identity documents
1702
- identity: ["password", "recoveryKey", "securityAnswers"],
1703
- // Financial accounts
1704
- bank_account: ["accountNumber", "routingNumber"],
1705
- investment: ["accountNumber"],
1706
- // Access codes
1707
- access_code: ["code", "pin"],
1708
- // Credentials (show last 4 of document number)
1709
- credential: ["documentNumber"]
1710
- };
1711
- var PARTIAL_REDACTION_FIELDS = ["documentNumber", "accountNumber"];
1712
- var REDACTED = "[REDACTED]";
1713
- function redactEntity(entity, entityType, mode) {
1714
- if (mode === "full") {
1715
- return entity;
1716
- }
1717
- const fieldsToRedact = REDACTION_RULES[entityType] || [];
1718
- if (fieldsToRedact.length === 0) {
1719
- return entity;
1720
- }
1721
- const redacted = { ...entity };
1722
- for (const field of fieldsToRedact) {
1723
- if (field in redacted && redacted[field] != null) {
1724
- const value = redacted[field];
1725
- if (PARTIAL_REDACTION_FIELDS.includes(field) && typeof value === "string" && value.length > 4) {
1726
- redacted[field] = `****${value.slice(-4)}`;
1727
- } else {
1728
- redacted[field] = REDACTED;
1729
- }
1730
- }
1731
- }
1732
- return redacted;
1733
- }
1738
+ var import_types42 = require("@modelcontextprotocol/sdk/types.js");
1734
1739
 
1735
1740
  // ../cache-sqlite/src/sqliteStore.ts
1736
1741
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
@@ -2417,166 +2422,2637 @@ async function getCacheStats() {
2417
2422
  const cache = getCache();
2418
2423
  return cache.getCacheStats();
2419
2424
  }
2425
+ async function downloadAndDecryptFile(client, householdId, fileId, entityId, entityType) {
2426
+ const keyType = getKeyTypeForEntity(entityType);
2427
+ const householdKeyBytes = householdKeysCache.get(`${householdId}:${keyType}`);
2428
+ if (!householdKeyBytes) {
2429
+ throw new Error(`No key available for ${householdId}:${keyType}`);
2430
+ }
2431
+ const fileInfo = await client.get(
2432
+ `/households/${householdId}/files/${fileId}`
2433
+ );
2434
+ if (!fileInfo.downloadUrl) {
2435
+ throw new Error("No download URL returned for file");
2436
+ }
2437
+ const response = await fetch(fileInfo.downloadUrl);
2438
+ if (!response.ok) {
2439
+ throw new Error(`Failed to download file: ${response.status}`);
2440
+ }
2441
+ const encryptedBytes = await response.arrayBuffer();
2442
+ const cryptoVersion = fileInfo.cryptoVersion ?? 1;
2443
+ const keyDerivationId = cryptoVersion === 2 ? fileId : fileInfo.entityId || entityId;
2444
+ const mimeType = fileInfo.fileType || "application/octet-stream";
2445
+ const decrypted = await decryptFileFromArrayBuffer(
2446
+ householdKeyBytes,
2447
+ keyDerivationId,
2448
+ entityType,
2449
+ encryptedBytes,
2450
+ mimeType
2451
+ );
2452
+ return {
2453
+ bytes: decrypted.bytes,
2454
+ dataBase64: base64Encode(decrypted.bytes),
2455
+ mimeType: decrypted.mimeType,
2456
+ fileName: fileInfo.fileName
2457
+ };
2458
+ }
2420
2459
 
2421
- // src/server.ts
2422
- async function startServer(mode) {
2423
- const config = loadConfig();
2424
- const privacyMode = mode || config.defaultMode;
2425
- console.error(`[MCP] Starting server in ${privacyMode} mode`);
2426
- const client = await getAuthenticatedClient();
2427
- if (!client) {
2428
- console.error("[MCP] Not logged in. Run: estatehelm login");
2429
- process.exit(1);
2430
- }
2431
- const privateKey = await getPrivateKey();
2432
- if (!privateKey) {
2433
- console.error("[MCP] Failed to load encryption keys. Run: estatehelm login");
2434
- process.exit(1);
2460
+ // src/resources/households.ts
2461
+ async function listHouseholds() {
2462
+ return getHouseholds();
2463
+ }
2464
+ async function getHousehold(householdId) {
2465
+ const households = await getHouseholds();
2466
+ return households.find((h) => h.id === householdId) || null;
2467
+ }
2468
+ async function getHouseholdSummary(householdId, privateKey, getDecryptedEntities2) {
2469
+ const households = await getHouseholds();
2470
+ const household = households.find((h) => h.id === householdId);
2471
+ if (!household) {
2472
+ return null;
2435
2473
  }
2436
- await initCache();
2437
- console.error("[MCP] Checking for updates...");
2438
- const synced = await syncIfNeeded(client, privateKey);
2439
- if (synced) {
2440
- console.error("[MCP] Cache updated");
2441
- } else {
2442
- console.error("[MCP] Cache is up to date");
2474
+ const entityTypes = [
2475
+ "pet",
2476
+ "property",
2477
+ "vehicle",
2478
+ "contact",
2479
+ "insurance",
2480
+ "bank_account",
2481
+ "investment",
2482
+ "subscription",
2483
+ "maintenance_task",
2484
+ "password",
2485
+ "access_code"
2486
+ ];
2487
+ const counts = {};
2488
+ for (const type of entityTypes) {
2489
+ const entities = await getDecryptedEntities2(householdId, type, privateKey);
2490
+ counts[type] = entities.length;
2443
2491
  }
2444
- const server = new import_server.Server(
2445
- {
2446
- name: "estatehelm",
2447
- version: "1.0.0"
2492
+ return {
2493
+ household: {
2494
+ id: household.id,
2495
+ name: household.name
2448
2496
  },
2449
- {
2450
- capabilities: {
2451
- resources: {},
2452
- tools: {},
2453
- prompts: {}
2497
+ counts,
2498
+ totalEntities: Object.values(counts).reduce((a, b) => a + b, 0)
2499
+ };
2500
+ }
2501
+
2502
+ // src/filter.ts
2503
+ var REDACTION_RULES = {
2504
+ // Password entries
2505
+ password: ["password", "notes"],
2506
+ // Identity documents
2507
+ identity: ["password", "recovery_key", "security_answers"],
2508
+ // Financial accounts
2509
+ bank_account: ["account_number", "routing_number"],
2510
+ investment: ["account_number"],
2511
+ // Access codes
2512
+ access_code: ["code", "pin"],
2513
+ // Credentials (show last 4 of document number)
2514
+ credential: ["document_number"]
2515
+ };
2516
+ var PARTIAL_REDACTION_FIELDS = ["document_number", "account_number"];
2517
+ var REDACTED = "[REDACTED]";
2518
+ function redactEntity(entity, entityType, mode) {
2519
+ if (mode === "full") {
2520
+ return entity;
2521
+ }
2522
+ const fieldsToRedact = REDACTION_RULES[entityType] || [];
2523
+ if (fieldsToRedact.length === 0) {
2524
+ return entity;
2525
+ }
2526
+ const redacted = { ...entity };
2527
+ for (const field of fieldsToRedact) {
2528
+ if (field in redacted && redacted[field] != null) {
2529
+ const value = redacted[field];
2530
+ if (PARTIAL_REDACTION_FIELDS.includes(field) && typeof value === "string" && value.length > 4) {
2531
+ redacted[field] = `****${value.slice(-4)}`;
2532
+ } else {
2533
+ redacted[field] = REDACTED;
2454
2534
  }
2455
2535
  }
2456
- );
2457
- server.setRequestHandler(import_types5.ListResourcesRequestSchema, async () => {
2458
- const households = await getHouseholds();
2459
- const resources = [
2460
- {
2461
- uri: "estatehelm://households",
2462
- name: "All Households",
2463
- description: "List of all households you have access to",
2464
- mimeType: "application/json"
2465
- }
2466
- ];
2467
- for (const household of households) {
2468
- resources.push({
2469
- uri: `estatehelm://households/${household.id}`,
2470
- name: household.name,
2471
- description: `Household: ${household.name}`,
2472
- mimeType: "application/json"
2473
- });
2474
- const entityTypes = [
2475
- "pet",
2476
- "property",
2477
- "vehicle",
2478
- "contact",
2479
- "insurance",
2480
- "bank_account",
2481
- "investment",
2482
- "subscription",
2483
- "maintenance_task",
2484
- "password",
2485
- "access_code",
2486
- "document",
2487
- "medical",
2488
- "prescription",
2489
- "credential",
2490
- "utility"
2491
- ];
2492
- for (const type of entityTypes) {
2493
- resources.push({
2494
- uri: `estatehelm://households/${household.id}/${type}`,
2495
- name: `${household.name} - ${formatEntityType(type)}`,
2496
- description: `${formatEntityType(type)} in ${household.name}`,
2497
- mimeType: "application/json"
2498
- });
2499
- }
2536
+ }
2537
+ return redacted;
2538
+ }
2539
+
2540
+ // ../list-data/src/computedFields.ts
2541
+ function getExpiryStatus(days) {
2542
+ if (days < 0) return "expired";
2543
+ if (days <= 30) return "expiring_soon";
2544
+ return "ok";
2545
+ }
2546
+
2547
+ // ../utils/src/dateHelpers.ts
2548
+ function calculateDetailedAge(dateOfBirth) {
2549
+ if (!dateOfBirth) return null;
2550
+ const birth = new Date(dateOfBirth);
2551
+ const today = /* @__PURE__ */ new Date();
2552
+ let years = today.getFullYear() - birth.getFullYear();
2553
+ let months = today.getMonth() - birth.getMonth();
2554
+ if (today.getDate() < birth.getDate()) {
2555
+ months--;
2556
+ }
2557
+ if (months < 0) {
2558
+ years--;
2559
+ months += 12;
2560
+ }
2561
+ const fractionalYears = years + months / 12;
2562
+ return { years, months, fractionalYears };
2563
+ }
2564
+ function calculateCatHumanYears(catAge) {
2565
+ if (catAge <= 0) return 0;
2566
+ if (catAge < 1) return Math.round(catAge * 15);
2567
+ if (catAge < 2) return Math.round(15 + (catAge - 1) * 9);
2568
+ return Math.round(24 + (catAge - 2) * 4);
2569
+ }
2570
+ function calculateDogHumanYears(dogAge, size = "medium") {
2571
+ if (dogAge <= 0) return 0;
2572
+ if (dogAge < 1) return Math.round(dogAge * 15);
2573
+ if (dogAge < 2) return Math.round(15 + (dogAge - 1) * 9);
2574
+ const yearlyRate = size === "small" ? 4 : size === "large" ? 6 : 5;
2575
+ return Math.round(24 + (dogAge - 2) * yearlyRate);
2576
+ }
2577
+ function parseDateLocal(dateStr) {
2578
+ const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
2579
+ if (match) {
2580
+ const [, year, month, day] = match;
2581
+ return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
2582
+ }
2583
+ return new Date(dateStr);
2584
+ }
2585
+ function daysUntil(dateStr) {
2586
+ const targetDate = parseDateLocal(dateStr);
2587
+ const today = /* @__PURE__ */ new Date();
2588
+ today.setHours(0, 0, 0, 0);
2589
+ const diffTime = targetDate.getTime() - today.getTime();
2590
+ return Math.ceil(diffTime / (1e3 * 60 * 60 * 24));
2591
+ }
2592
+
2593
+ // ../utils/src/search/searchEngine.ts
2594
+ var DEFAULT_OPTIONS = {
2595
+ minQueryLength: 1,
2596
+ maxPerGroup: 5,
2597
+ maxTotal: 25,
2598
+ includeTypes: [],
2599
+ excludeTypes: []
2600
+ };
2601
+ function scoreMatch(query, text) {
2602
+ if (!query || !text) return 0;
2603
+ const queryLower = query.toLowerCase().trim();
2604
+ const textLower = text.toLowerCase();
2605
+ if (queryLower.length === 0) return 0;
2606
+ if (textLower === queryLower) {
2607
+ return 100;
2608
+ }
2609
+ if (textLower.startsWith(queryLower)) {
2610
+ return 75;
2611
+ }
2612
+ const words = textLower.split(/\s+/);
2613
+ for (const word of words) {
2614
+ if (word.startsWith(queryLower)) {
2615
+ return 50;
2500
2616
  }
2501
- return { resources };
2502
- });
2503
- server.setRequestHandler(import_types5.ReadResourceRequestSchema, async (request) => {
2504
- const uri = request.params.uri;
2505
- const parsed = parseResourceUri(uri);
2506
- if (!parsed) {
2507
- throw new Error(`Invalid resource URI: ${uri}`);
2617
+ }
2618
+ if (textLower.includes(queryLower)) {
2619
+ return 25;
2620
+ }
2621
+ return 0;
2622
+ }
2623
+ function scoreEntity(entity, query) {
2624
+ const matchedFields = [];
2625
+ let bestScore = 0;
2626
+ const nameScore = scoreMatch(query, entity.name);
2627
+ if (nameScore > 0) {
2628
+ matchedFields.push("name");
2629
+ bestScore = Math.max(bestScore, nameScore);
2630
+ }
2631
+ if (entity.subtitle) {
2632
+ const subtitleScore = scoreMatch(query, entity.subtitle);
2633
+ if (subtitleScore > 0) {
2634
+ matchedFields.push("subtitle");
2635
+ bestScore = Math.max(bestScore, subtitleScore * 0.9);
2508
2636
  }
2509
- let content;
2510
- if (parsed.type === "households" && !parsed.householdId) {
2511
- content = await getHouseholds();
2512
- } else if (parsed.type === "households" && parsed.householdId && !parsed.entityType) {
2513
- const households = await getHouseholds();
2514
- content = households.find((h) => h.id === parsed.householdId);
2515
- if (!content) {
2516
- throw new Error(`Household not found: ${parsed.householdId}`);
2637
+ }
2638
+ if (entity.keywords && entity.keywords.length > 0) {
2639
+ for (const keyword of entity.keywords) {
2640
+ const keywordScore = scoreMatch(query, keyword);
2641
+ if (keywordScore > 0) {
2642
+ if (!matchedFields.includes("keywords")) {
2643
+ matchedFields.push("keywords");
2644
+ }
2645
+ bestScore = Math.max(bestScore, keywordScore * 0.8);
2517
2646
  }
2518
- } else if (parsed.householdId && parsed.entityType) {
2519
- const entities = await getDecryptedEntities(parsed.householdId, parsed.entityType, privateKey);
2520
- content = entities.map((e) => redactEntity(e, parsed.entityType, privacyMode));
2521
- } else {
2522
- throw new Error(`Unsupported resource: ${uri}`);
2523
2647
  }
2524
- return {
2525
- contents: [
2526
- {
2527
- uri,
2528
- mimeType: "application/json",
2529
- text: JSON.stringify(content, null, 2)
2530
- }
2531
- ]
2532
- };
2648
+ }
2649
+ return { score: bestScore, matchedFields };
2650
+ }
2651
+ function searchEntities(entities, query, options) {
2652
+ const opts = { ...DEFAULT_OPTIONS, ...options };
2653
+ const trimmedQuery = query.trim();
2654
+ if (trimmedQuery.length < opts.minQueryLength) {
2655
+ return [];
2656
+ }
2657
+ let filteredEntities = entities;
2658
+ if (opts.includeTypes && opts.includeTypes.length > 0) {
2659
+ filteredEntities = filteredEntities.filter(
2660
+ (e) => opts.includeTypes.includes(e.entityType)
2661
+ );
2662
+ }
2663
+ if (opts.excludeTypes && opts.excludeTypes.length > 0) {
2664
+ filteredEntities = filteredEntities.filter(
2665
+ (e) => !opts.excludeTypes.includes(e.entityType)
2666
+ );
2667
+ }
2668
+ if (trimmedQuery.length === 0) {
2669
+ const results2 = filteredEntities.map((entity) => ({
2670
+ entity,
2671
+ score: 1,
2672
+ // Default score for unfiltered results
2673
+ matchedFields: []
2674
+ }));
2675
+ results2.sort((a, b) => (a.entity?.name || "").localeCompare(b.entity?.name || ""));
2676
+ return results2.slice(0, opts.maxTotal);
2677
+ }
2678
+ const results = [];
2679
+ for (const entity of filteredEntities) {
2680
+ const { score, matchedFields } = scoreEntity(entity, trimmedQuery);
2681
+ if (score > 0) {
2682
+ results.push({ entity, score, matchedFields });
2683
+ }
2684
+ }
2685
+ results.sort((a, b) => {
2686
+ if (b.score !== a.score) {
2687
+ return b.score - a.score;
2688
+ }
2689
+ return (a.entity?.name || "").localeCompare(b.entity?.name || "");
2533
2690
  });
2534
- server.setRequestHandler(import_types5.ListToolsRequestSchema, async () => {
2535
- return {
2536
- tools: [
2537
- {
2538
- name: "search_entities",
2539
- description: "Search across all entities in EstateHelm",
2540
- inputSchema: {
2541
- type: "object",
2542
- properties: {
2543
- query: {
2544
- type: "string",
2545
- description: "Search query"
2546
- },
2547
- householdId: {
2548
- type: "string",
2549
- description: "Optional: Limit search to a specific household"
2550
- },
2551
- entityType: {
2552
- type: "string",
2553
- description: "Optional: Limit search to a specific entity type"
2554
- }
2555
- },
2556
- required: ["query"]
2557
- }
2558
- },
2559
- {
2560
- name: "get_household_summary",
2561
- description: "Get a summary of a household including counts and key dates",
2562
- inputSchema: {
2563
- type: "object",
2564
- properties: {
2565
- householdId: {
2566
- type: "string",
2567
- description: "The household ID"
2568
- }
2569
- },
2570
- required: ["householdId"]
2571
- }
2572
- },
2573
- {
2574
- name: "get_expiring_items",
2575
- description: "Get items expiring within a given number of days",
2576
- inputSchema: {
2577
- type: "object",
2578
- properties: {
2579
- days: {
2691
+ return results.slice(0, opts.maxTotal);
2692
+ }
2693
+
2694
+ // ../utils/src/search/entityIndexer.ts
2695
+ function formatLabel(value) {
2696
+ if (!value) return "";
2697
+ return value.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2698
+ }
2699
+ function getVehicleName(vehicle) {
2700
+ if (vehicle.name) return vehicle.name;
2701
+ const parts = [vehicle.year, vehicle.make, vehicle.model].filter(Boolean);
2702
+ return parts.length > 0 ? parts.join(" ") : "Vehicle";
2703
+ }
2704
+ function getContactName(contact) {
2705
+ const fullName = [contact.first_name, contact.last_name].filter(Boolean).join(" ");
2706
+ if (fullName) return fullName;
2707
+ if (contact.company_name) return contact.company_name;
2708
+ return "Contact";
2709
+ }
2710
+ function getSubscriptionName(sub) {
2711
+ return sub.custom_name || formatLabel(sub.provider) || "Subscription";
2712
+ }
2713
+ function getServiceName(service) {
2714
+ return service.service_name || service.name || "Service";
2715
+ }
2716
+ function getPropertyName(property) {
2717
+ if (property.name) return property.name;
2718
+ if (property.street_address) {
2719
+ return property.city ? `${property.street_address}, ${property.city}` : property.street_address;
2720
+ }
2721
+ return "Property";
2722
+ }
2723
+ function getPetName(pet) {
2724
+ return pet.name || "Pet";
2725
+ }
2726
+ function getAccessCodeName(code) {
2727
+ return code.name || code.description || "Access Code";
2728
+ }
2729
+ function getFinancialAccountName(account) {
2730
+ return account.name || account.account_name || account.nickname || (account.institution ? `${account.institution} Account` : "Financial Account");
2731
+ }
2732
+ function getInsuranceName(policy) {
2733
+ return policy.name || policy.policy_name || policy.provider || formatLabel(policy.type) || "Insurance";
2734
+ }
2735
+ function getValuableName(valuable) {
2736
+ return valuable.name || valuable.item_name || valuable.description || "Valuable";
2737
+ }
2738
+ function getMaintenanceTaskName(task) {
2739
+ return task.name || task.title || task.task_name || "Maintenance Task";
2740
+ }
2741
+ function getDeviceName(device) {
2742
+ return device.name || device.device_name || "Device";
2743
+ }
2744
+ function getPetVetVisitName(visit) {
2745
+ return visit.name || visit.clinic_name || visit.reason || "Vet Visit";
2746
+ }
2747
+ function getPetHealthName(record) {
2748
+ return record.name || (record.record_type ? formatLabel(record.record_type) : "Health Record");
2749
+ }
2750
+ function getVehicleMaintenanceName(record) {
2751
+ return record.name || record.service_type || record.description || "Maintenance Record";
2752
+ }
2753
+ function getVehicleServiceVisitName(visit) {
2754
+ return visit.name || visit.shop_name || visit.description || "Service Visit";
2755
+ }
2756
+ function getEntityDisplayName(entityType, data) {
2757
+ switch (entityType) {
2758
+ case "property":
2759
+ return getPropertyName(data);
2760
+ case "vehicle":
2761
+ return getVehicleName(data);
2762
+ case "pet":
2763
+ return getPetName(data);
2764
+ case "contact":
2765
+ return getContactName(data);
2766
+ case "subscription":
2767
+ return getSubscriptionName(data);
2768
+ case "service":
2769
+ return getServiceName(data);
2770
+ case "insurance":
2771
+ return getInsuranceName(data);
2772
+ case "valuable":
2773
+ return getValuableName(data);
2774
+ case "financial_account":
2775
+ return getFinancialAccountName(data);
2776
+ case "access_code":
2777
+ return getAccessCodeName(data);
2778
+ case "maintenance_task":
2779
+ return getMaintenanceTaskName(data);
2780
+ case "device":
2781
+ return getDeviceName(data);
2782
+ case "pet_vet_visit":
2783
+ return getPetVetVisitName(data);
2784
+ case "pet_health":
2785
+ return getPetHealthName(data);
2786
+ case "vehicle_maintenance":
2787
+ return getVehicleMaintenanceName(data);
2788
+ case "vehicle_service_visit":
2789
+ return getVehicleServiceVisitName(data);
2790
+ case "home_improvement":
2791
+ return data.name || data.title || data.project_name || "Home Improvement";
2792
+ case "credential":
2793
+ return data.name || data.title || "Credential";
2794
+ case "password":
2795
+ return data.name || data.site_name || data.title || "Password";
2796
+ case "legal":
2797
+ return data.name || data.title || "Legal Document";
2798
+ case "military_record":
2799
+ return data.name || data.title || "Military Record";
2800
+ case "education_record":
2801
+ return data.name || data.institution || data.title || "Education Record";
2802
+ case "travel":
2803
+ return data.name || data.document_type || "Travel Document";
2804
+ case "identity":
2805
+ return data.name || data.document_type || "Identity Document";
2806
+ case "document":
2807
+ return data.name || data.title || data.file_name || "Document";
2808
+ default:
2809
+ return data.name || data.title || entityType;
2810
+ }
2811
+ }
2812
+ function indexProperty(property) {
2813
+ const subtitle = property.city && property.state ? `${property.city}, ${property.state}` : property.street_address || "Property";
2814
+ return {
2815
+ id: property.id,
2816
+ entityType: "property",
2817
+ name: property.name,
2818
+ subtitle,
2819
+ // Synonyms: house, home for natural language searches
2820
+ keywords: [property.street_address, property.city, property.state, "house", "home"].filter(Boolean),
2821
+ icon: "Home",
2822
+ route: `/property?id=${property.id}`,
2823
+ routeParams: { id: property.id }
2824
+ };
2825
+ }
2826
+ function indexVehicle(vehicle) {
2827
+ const subtitle = formatLabel(vehicle.vehicle_type) || "Vehicle";
2828
+ const synonyms = [];
2829
+ if (vehicle.vehicle_type === "car" || vehicle.vehicle_type === "sedan" || vehicle.vehicle_type === "suv" || vehicle.vehicle_type === "truck") {
2830
+ synonyms.push("car", "auto", "automobile");
2831
+ }
2832
+ if (vehicle.vehicle_type === "motorcycle") {
2833
+ synonyms.push("bike", "motorbike");
2834
+ }
2835
+ if (vehicle.vehicle_type === "boat") {
2836
+ synonyms.push("watercraft");
2837
+ }
2838
+ const keywords = [vehicle.make, vehicle.model, vehicle.license_plate, ...synonyms].filter(Boolean);
2839
+ return {
2840
+ id: vehicle.id,
2841
+ entityType: "vehicle",
2842
+ name: getVehicleName(vehicle),
2843
+ subtitle,
2844
+ keywords,
2845
+ icon: "Car",
2846
+ route: `/vehicle?id=${vehicle.id}`,
2847
+ routeParams: { id: vehicle.id }
2848
+ };
2849
+ }
2850
+ function indexPet(pet) {
2851
+ const speciesLabel = formatLabel(pet.species);
2852
+ const subtitle = pet.breed ? `${speciesLabel} - ${pet.breed}` : speciesLabel || "Pet";
2853
+ const synonyms = [];
2854
+ if (pet.species === "dog" || pet.species === "canine") {
2855
+ synonyms.push("dog", "puppy", "pup");
2856
+ } else if (pet.species === "cat" || pet.species === "feline") {
2857
+ synonyms.push("cat", "kitty", "kitten");
2858
+ } else if (pet.species === "bird") {
2859
+ synonyms.push("bird", "parrot");
2860
+ } else if (pet.species === "fish") {
2861
+ synonyms.push("fish", "aquarium");
2862
+ }
2863
+ return {
2864
+ id: pet.id,
2865
+ entityType: "pet",
2866
+ name: pet.name,
2867
+ subtitle,
2868
+ keywords: [pet.breed, pet.species, ...synonyms].filter(Boolean),
2869
+ icon: "PawPrint",
2870
+ route: `/pet?id=${pet.id}`,
2871
+ routeParams: { id: pet.id }
2872
+ };
2873
+ }
2874
+ function indexContact(contact, linkedEntities) {
2875
+ const subtitle = contact.specialty ? `${formatLabel(contact.type)} - ${contact.specialty}` : formatLabel(contact.type) || "Contact";
2876
+ const linkedNames = linkedEntities?.map((e) => e.name) || [];
2877
+ const linkedRoles = linkedEntities?.map((e) => formatLabel(e.role)) || [];
2878
+ return {
2879
+ id: contact.id,
2880
+ entityType: "contact",
2881
+ name: getContactName(contact),
2882
+ subtitle,
2883
+ keywords: [
2884
+ contact.company_name,
2885
+ contact.first_name,
2886
+ contact.last_name,
2887
+ contact.specialty,
2888
+ ...linkedNames,
2889
+ ...linkedRoles
2890
+ ].filter(Boolean),
2891
+ icon: "Phone",
2892
+ route: `/contact?id=${contact.id}`,
2893
+ routeParams: { id: contact.id }
2894
+ };
2895
+ }
2896
+ function indexSubscription(sub) {
2897
+ const subtitle = formatLabel(sub.category) || "Subscription";
2898
+ const displayName = getSubscriptionName(sub);
2899
+ const keywords = [];
2900
+ if (sub.provider) {
2901
+ keywords.push(sub.provider);
2902
+ const formatted = formatLabel(sub.provider);
2903
+ if (formatted !== sub.provider) {
2904
+ keywords.push(formatted);
2905
+ }
2906
+ }
2907
+ if (sub.custom_name) {
2908
+ keywords.push(sub.custom_name);
2909
+ }
2910
+ if (sub.category) {
2911
+ keywords.push(sub.category);
2912
+ }
2913
+ const formattedProvider = formatLabel(sub.provider);
2914
+ const searchableName = sub.provider && formattedProvider !== displayName ? `${displayName} (${formattedProvider})` : displayName;
2915
+ return {
2916
+ id: sub.id,
2917
+ entityType: "subscription",
2918
+ name: searchableName,
2919
+ subtitle,
2920
+ keywords,
2921
+ icon: "CreditCard",
2922
+ route: `/subscription?id=${sub.id}`,
2923
+ routeParams: { id: sub.id }
2924
+ };
2925
+ }
2926
+ function indexService(service) {
2927
+ const subtitle = formatLabel(service.service_type) || "Service";
2928
+ return {
2929
+ id: service.id,
2930
+ entityType: "service",
2931
+ name: getServiceName(service),
2932
+ subtitle,
2933
+ keywords: [service.provider, service.service_type].filter(Boolean),
2934
+ icon: "Wrench",
2935
+ route: `/service?id=${service.id}`,
2936
+ routeParams: { id: service.id }
2937
+ };
2938
+ }
2939
+ function indexInsurance(policy, coveredAssetNames) {
2940
+ const subtitle = formatLabel(policy.type) || "Insurance";
2941
+ const keywords = [policy.policy_number, policy.type, ...coveredAssetNames || []].filter(Boolean);
2942
+ return {
2943
+ id: policy.id,
2944
+ entityType: "insurance",
2945
+ name: policy.provider || "Insurance Policy",
2946
+ subtitle,
2947
+ keywords,
2948
+ icon: "Shield",
2949
+ route: `/insurance?id=${policy.id}`,
2950
+ routeParams: { id: policy.id }
2951
+ };
2952
+ }
2953
+ function indexValuable(valuable) {
2954
+ const categoryLabel = valuable.category === "other" && valuable.other_category ? valuable.other_category : formatLabel(valuable.category);
2955
+ const subtitle = categoryLabel || "Valuable";
2956
+ return {
2957
+ id: valuable.id,
2958
+ entityType: "valuable",
2959
+ name: valuable.name,
2960
+ subtitle,
2961
+ keywords: [valuable.category, valuable.other_category].filter(Boolean),
2962
+ icon: "Gem",
2963
+ route: `/valuables?id=${valuable.id}`,
2964
+ routeParams: { id: valuable.id }
2965
+ };
2966
+ }
2967
+ function indexFinancialAccount(account, ownerNames) {
2968
+ const typeLabel = formatLabel(account.account_type) || "Financial Account";
2969
+ let subtitle = typeLabel;
2970
+ if (ownerNames && ownerNames.length > 0) {
2971
+ subtitle = `${typeLabel} \u2022 ${ownerNames.join(", ")}`;
2972
+ }
2973
+ return {
2974
+ id: account.id,
2975
+ entityType: "financial_account",
2976
+ name: account.nickname || account.institution || "Financial Account",
2977
+ subtitle,
2978
+ keywords: [account.institution, account.account_type, ...ownerNames || []].filter(Boolean),
2979
+ icon: "Landmark",
2980
+ route: `/financial?id=${account.id}`,
2981
+ routeParams: { id: account.id }
2982
+ };
2983
+ }
2984
+ function indexCredential(credential, personName) {
2985
+ const subtypeLabel = formatLabel(credential.credential_subtype);
2986
+ const typeLabel = subtypeLabel || formatLabel(credential.credential_type) || "Credential";
2987
+ const subtitle = personName ? `${typeLabel} \u2022 ${personName}` : typeLabel;
2988
+ const synonyms = ["id", "identification"];
2989
+ const subtype = credential.credential_subtype?.toLowerCase() || "";
2990
+ const type = credential.credential_type?.toLowerCase() || "";
2991
+ if (subtype.includes("passport")) {
2992
+ synonyms.push("passport", "travel document");
2993
+ }
2994
+ if (subtype.includes("driver") || subtype.includes("license")) {
2995
+ synonyms.push("license", "drivers license", "DL", "driving license");
2996
+ }
2997
+ if (subtype.includes("ssn") || subtype.includes("social_security")) {
2998
+ synonyms.push("social security", "SSN", "social security number");
2999
+ }
3000
+ if (type.includes("professional") || type.includes("license")) {
3001
+ synonyms.push("license", "certification", "cert");
3002
+ }
3003
+ if (subtype.includes("birth") || subtype.includes("certificate")) {
3004
+ synonyms.push("birth certificate", "certificate");
3005
+ }
3006
+ return {
3007
+ id: credential.id,
3008
+ entityType: "credential",
3009
+ name: credential.name,
3010
+ subtitle,
3011
+ keywords: [credential.credential_type, credential.credential_subtype, credential.issuing_authority, personName, ...synonyms].filter(Boolean),
3012
+ icon: "CreditCard",
3013
+ route: `/credentials?id=${credential.id}`,
3014
+ routeParams: { id: credential.id }
3015
+ };
3016
+ }
3017
+ function indexPetVetVisit(visit, petName) {
3018
+ const procedureNames = visit.procedures?.map((p) => p.name).filter(Boolean).join(", ");
3019
+ let subtitle = petName || "Vet Visit";
3020
+ if (petName && procedureNames) {
3021
+ subtitle = `${petName} \u2022 ${procedureNames}`;
3022
+ } else if (procedureNames) {
3023
+ subtitle = procedureNames;
3024
+ }
3025
+ return {
3026
+ id: visit.id,
3027
+ entityType: "pet_vet_visit",
3028
+ name: visit.name || `${petName || "Pet"} - Vet Visit`,
3029
+ subtitle,
3030
+ keywords: [petName, ...visit.procedures?.map((p) => p.name).filter(Boolean) || []].filter(Boolean),
3031
+ icon: "Stethoscope",
3032
+ route: `/pet?tab=health&id=${visit.id}`,
3033
+ routeParams: { tab: "health", id: visit.id }
3034
+ };
3035
+ }
3036
+ function indexVehicleService(service, vehicleName) {
3037
+ const serviceNames = service.services?.map((s) => s.name).filter(Boolean).join(", ");
3038
+ let subtitle = vehicleName || "Service Record";
3039
+ if (vehicleName && serviceNames) {
3040
+ subtitle = `${vehicleName} \u2022 ${serviceNames}`;
3041
+ } else if (serviceNames) {
3042
+ subtitle = serviceNames;
3043
+ }
3044
+ return {
3045
+ id: service.id,
3046
+ entityType: "vehicle_service",
3047
+ name: service.name || `${vehicleName || "Vehicle"} - Service`,
3048
+ subtitle,
3049
+ keywords: [vehicleName, ...service.services?.map((s) => s.name).filter(Boolean) || []].filter(Boolean),
3050
+ icon: "FileText",
3051
+ route: `/vehicle-maintenance?id=${service.id}`,
3052
+ routeParams: { id: service.id }
3053
+ };
3054
+ }
3055
+ function indexMaintenanceTask(task, assetName) {
3056
+ const subtitle = assetName ? `${formatLabel(task.category)} - ${assetName}` : formatLabel(task.category) || "Maintenance";
3057
+ return {
3058
+ id: task.id,
3059
+ entityType: "maintenance_task",
3060
+ name: task.name || task.title || "Maintenance Task",
3061
+ subtitle,
3062
+ keywords: [task.category].filter(Boolean),
3063
+ icon: "ClipboardList",
3064
+ route: `/maintenance?id=${task.id}`,
3065
+ routeParams: { id: task.id }
3066
+ };
3067
+ }
3068
+ function indexLegalDocument(doc, residentNames) {
3069
+ const typeLabel = formatLabel(doc.type) || "Legal Document";
3070
+ let subtitle = typeLabel;
3071
+ if (residentNames && residentNames.length > 0) {
3072
+ subtitle = `${typeLabel} \u2022 ${residentNames.join(", ")}`;
3073
+ }
3074
+ const synonyms = ["doc", "document", "legal", "paperwork"];
3075
+ return {
3076
+ id: doc.id,
3077
+ entityType: "legal_document",
3078
+ name: doc.name,
3079
+ subtitle,
3080
+ keywords: [doc.type, ...residentNames || [], ...synonyms].filter(Boolean),
3081
+ icon: "FileText",
3082
+ route: `/legal?id=${doc.id}`,
3083
+ routeParams: { id: doc.id }
3084
+ };
3085
+ }
3086
+ function indexAccessCode(code, propertyName) {
3087
+ const subtitle = propertyName ? `${formatLabel(code.code_type)} - ${propertyName}` : formatLabel(code.code_type) || "Access Code";
3088
+ return {
3089
+ id: code.id,
3090
+ entityType: "access_code",
3091
+ name: code.name,
3092
+ subtitle,
3093
+ keywords: [code.code_type].filter(Boolean),
3094
+ icon: "Key",
3095
+ route: `/property?tab=access&id=${code.id}`,
3096
+ routeParams: { tab: "access", id: code.id }
3097
+ };
3098
+ }
3099
+ function indexDevice(device) {
3100
+ const subtitle = device.brand ? `${formatLabel(device.device_type)} - ${device.brand}` : formatLabel(device.device_type) || "Device";
3101
+ return {
3102
+ id: device.id,
3103
+ entityType: "device",
3104
+ name: device.name,
3105
+ subtitle,
3106
+ keywords: [device.device_type, device.brand, device.model].filter(Boolean),
3107
+ icon: "Wifi",
3108
+ route: `/device?id=${device.id}`,
3109
+ routeParams: { id: device.id }
3110
+ };
3111
+ }
3112
+ function indexPerson(person) {
3113
+ const subtitle = formatLabel(person.relationship_type) || formatLabel(person.person_type) || "Person";
3114
+ return {
3115
+ id: person.id,
3116
+ entityType: "person",
3117
+ name: person.name,
3118
+ subtitle,
3119
+ keywords: [person.person_type, person.relationship_type].filter(Boolean),
3120
+ icon: "User",
3121
+ route: `/people?id=${person.id}`,
3122
+ routeParams: { id: person.id }
3123
+ };
3124
+ }
3125
+ function indexHealthRecord(record, ownerName) {
3126
+ const subtitle = ownerName ? `${formatLabel(record.record_type)} - ${ownerName}` : formatLabel(record.record_type) || "Health Record";
3127
+ return {
3128
+ id: record.id,
3129
+ entityType: "health_record",
3130
+ name: record.name,
3131
+ subtitle,
3132
+ keywords: [record.record_type, record.provider].filter(Boolean),
3133
+ icon: "Heart",
3134
+ route: `/records?type=health_record&id=${record.id}`,
3135
+ routeParams: { id: record.id }
3136
+ };
3137
+ }
3138
+ function indexEducationRecord(record, personName) {
3139
+ const subtitle = personName ? `${formatLabel(record.record_type)} - ${personName}` : record.institution || formatLabel(record.record_type) || "Education Record";
3140
+ return {
3141
+ id: record.id,
3142
+ entityType: "education_record",
3143
+ name: record.name,
3144
+ subtitle,
3145
+ keywords: [record.record_type, record.institution, record.level, record.field_of_study].filter(Boolean),
3146
+ icon: "GraduationCap",
3147
+ route: `/records?type=education_record&id=${record.id}`,
3148
+ routeParams: { id: record.id }
3149
+ };
3150
+ }
3151
+ function indexMilitaryRecord(record, personName) {
3152
+ const branchLabel = formatLabel(record.branch);
3153
+ const subtitle = personName ? `${branchLabel || formatLabel(record.record_type)} - ${personName}` : branchLabel || formatLabel(record.record_type) || "Military Record";
3154
+ return {
3155
+ id: record.id,
3156
+ entityType: "military_record",
3157
+ name: record.name,
3158
+ subtitle,
3159
+ keywords: [record.record_type, record.branch, record.rank].filter(Boolean),
3160
+ icon: "Medal",
3161
+ route: `/records?type=military_record&id=${record.id}`,
3162
+ routeParams: { id: record.id }
3163
+ };
3164
+ }
3165
+ function indexHomeImprovement(improvement, propertyName) {
3166
+ const subtitle = propertyName ? `${formatLabel(improvement.improvement_type)} - ${propertyName}` : formatLabel(improvement.improvement_type) || "Home Improvement";
3167
+ return {
3168
+ id: improvement.id,
3169
+ entityType: "home_improvement",
3170
+ name: improvement.name,
3171
+ subtitle,
3172
+ keywords: [improvement.improvement_type].filter(Boolean),
3173
+ icon: "Hammer",
3174
+ route: `/home-improvement/${improvement.id}`,
3175
+ routeParams: { id: improvement.id }
3176
+ };
3177
+ }
3178
+ function indexTaxYear(taxYear, filerNames) {
3179
+ let subtitle = taxYear.country || "Tax Year";
3180
+ if (filerNames && filerNames.length > 0) {
3181
+ subtitle = filerNames.join(", ");
3182
+ }
3183
+ return {
3184
+ id: taxYear.id,
3185
+ entityType: "tax_year",
3186
+ name: `${taxYear.year} Tax Year`,
3187
+ subtitle,
3188
+ keywords: [taxYear.country, String(taxYear.year), ...filerNames || []].filter(Boolean),
3189
+ icon: "Receipt",
3190
+ route: `/taxes?id=${taxYear.id}`,
3191
+ routeParams: { id: taxYear.id }
3192
+ };
3193
+ }
3194
+ function indexMembershipRecord(record, personName) {
3195
+ const typeLabel = formatLabel(record.membership_type);
3196
+ let subtitle = typeLabel || "Membership";
3197
+ if (personName) {
3198
+ subtitle = record.level ? `${typeLabel} \u2022 ${record.level} \u2022 ${personName}` : `${typeLabel} \u2022 ${personName}`;
3199
+ } else if (record.level) {
3200
+ subtitle = `${typeLabel} \u2022 ${record.level}`;
3201
+ }
3202
+ return {
3203
+ id: record.id,
3204
+ entityType: "membership_record",
3205
+ name: record.name,
3206
+ subtitle,
3207
+ keywords: [record.membership_type, record.membership_number, record.level, record.alliance, personName].filter(Boolean),
3208
+ icon: "Ticket",
3209
+ route: `/records?type=membership_record&id=${record.id}`,
3210
+ routeParams: { id: record.id }
3211
+ };
3212
+ }
3213
+ function indexAllEntities(data) {
3214
+ const entities = [];
3215
+ const propertyNames = /* @__PURE__ */ new Map();
3216
+ const vehicleNames = /* @__PURE__ */ new Map();
3217
+ const petNames = /* @__PURE__ */ new Map();
3218
+ const personNames = /* @__PURE__ */ new Map();
3219
+ data.properties?.forEach((p) => propertyNames.set(p.id, p.name));
3220
+ data.vehicles?.forEach((v) => vehicleNames.set(v.id, getVehicleName(v)));
3221
+ data.pets?.forEach((p) => petNames.set(p.id, p.name));
3222
+ data.people?.forEach((p) => personNames.set(p.id, p.name));
3223
+ const contactLinkedEntities = /* @__PURE__ */ new Map();
3224
+ const addContactLink = (contactId, name, role) => {
3225
+ const existing = contactLinkedEntities.get(contactId) || [];
3226
+ existing.push({ name, role });
3227
+ contactLinkedEntities.set(contactId, existing);
3228
+ };
3229
+ data.pets?.forEach((pet) => {
3230
+ pet.contact_relationships?.forEach((rel) => {
3231
+ addContactLink(rel.contact_id, pet.name, rel.role);
3232
+ });
3233
+ });
3234
+ data.vehicles?.forEach((vehicle) => {
3235
+ const name = getVehicleName(vehicle);
3236
+ vehicle.contact_relationships?.forEach((rel) => {
3237
+ addContactLink(rel.contact_id, name, rel.role);
3238
+ });
3239
+ });
3240
+ data.properties?.forEach((property) => {
3241
+ property.contact_relationships?.forEach((rel) => {
3242
+ addContactLink(rel.contact_id, property.name, rel.role);
3243
+ });
3244
+ });
3245
+ data.properties?.forEach((p) => entities.push(indexProperty(p)));
3246
+ data.vehicles?.forEach((v) => entities.push(indexVehicle(v)));
3247
+ data.pets?.forEach((p) => entities.push(indexPet(p)));
3248
+ data.contacts?.forEach((c) => {
3249
+ const linkedEntities = contactLinkedEntities.get(c.id);
3250
+ entities.push(indexContact(c, linkedEntities));
3251
+ });
3252
+ data.subscriptions?.forEach((s) => entities.push(indexSubscription(s)));
3253
+ data.services?.forEach((s) => entities.push(indexService(s)));
3254
+ data.insurancePolicies?.forEach((policy) => {
3255
+ const coveredAssetNames = [];
3256
+ policy.relationships?.forEach((rel) => {
3257
+ if (rel.entity_type === "property") {
3258
+ const name = propertyNames.get(rel.entity_id);
3259
+ if (name) coveredAssetNames.push(name);
3260
+ } else if (rel.entity_type === "vehicle") {
3261
+ const name = vehicleNames.get(rel.entity_id);
3262
+ if (name) coveredAssetNames.push(name);
3263
+ } else if (rel.entity_type === "pet") {
3264
+ const name = petNames.get(rel.entity_id);
3265
+ if (name) coveredAssetNames.push(name);
3266
+ }
3267
+ });
3268
+ if (policy.property_id) {
3269
+ const name = propertyNames.get(policy.property_id);
3270
+ if (name && !coveredAssetNames.includes(name)) coveredAssetNames.push(name);
3271
+ }
3272
+ if (policy.vehicle_id) {
3273
+ const name = vehicleNames.get(policy.vehicle_id);
3274
+ if (name && !coveredAssetNames.includes(name)) coveredAssetNames.push(name);
3275
+ }
3276
+ if (policy.pet_id) {
3277
+ const name = petNames.get(policy.pet_id);
3278
+ if (name && !coveredAssetNames.includes(name)) coveredAssetNames.push(name);
3279
+ }
3280
+ entities.push(indexInsurance(policy, coveredAssetNames.length > 0 ? coveredAssetNames : void 0));
3281
+ });
3282
+ data.valuables?.forEach((v) => entities.push(indexValuable(v)));
3283
+ data.financialAccounts?.forEach((a) => {
3284
+ const ownerNames = a.owner_ids?.map((id) => personNames.get(id)).filter(Boolean);
3285
+ entities.push(indexFinancialAccount(a, ownerNames));
3286
+ });
3287
+ data.credentials?.forEach((c) => {
3288
+ const personName = c.person_id ? personNames.get(c.person_id) : void 0;
3289
+ entities.push(indexCredential(c, personName));
3290
+ });
3291
+ data.petVetVisits?.forEach((v) => {
3292
+ const petName = v.pet_id ? petNames.get(v.pet_id) : void 0;
3293
+ entities.push(indexPetVetVisit(v, petName));
3294
+ });
3295
+ data.vehicleServices?.forEach((s) => {
3296
+ const vehicleName = s.vehicle_id ? vehicleNames.get(s.vehicle_id) : void 0;
3297
+ entities.push(indexVehicleService(s, vehicleName));
3298
+ });
3299
+ data.maintenanceTasks?.forEach((t) => {
3300
+ let assetName;
3301
+ if (t.property_id) assetName = propertyNames.get(t.property_id);
3302
+ else if (t.vehicle_id) assetName = vehicleNames.get(t.vehicle_id);
3303
+ entities.push(indexMaintenanceTask(t, assetName));
3304
+ });
3305
+ data.legalDocuments?.forEach((d) => {
3306
+ const residentNames = d.resident_ids?.map((id) => personNames.get(id)).filter(Boolean);
3307
+ entities.push(indexLegalDocument(d, residentNames));
3308
+ });
3309
+ data.accessCodes?.forEach((c) => {
3310
+ const propertyName = c.property_id ? propertyNames.get(c.property_id) : void 0;
3311
+ entities.push(indexAccessCode(c, propertyName));
3312
+ });
3313
+ data.devices?.forEach((d) => entities.push(indexDevice(d)));
3314
+ data.people?.forEach((p) => entities.push(indexPerson(p)));
3315
+ data.healthRecords?.forEach((r) => {
3316
+ let ownerName;
3317
+ if (r.person_id) ownerName = personNames.get(r.person_id);
3318
+ else if (r.pet_id) ownerName = petNames.get(r.pet_id);
3319
+ entities.push(indexHealthRecord(r, ownerName));
3320
+ });
3321
+ data.educationRecords?.forEach((r) => {
3322
+ const personName = r.person_id ? personNames.get(r.person_id) : void 0;
3323
+ entities.push(indexEducationRecord(r, personName));
3324
+ });
3325
+ data.militaryRecords?.forEach((r) => {
3326
+ const personName = r.person_id ? personNames.get(r.person_id) : void 0;
3327
+ entities.push(indexMilitaryRecord(r, personName));
3328
+ });
3329
+ data.membershipRecords?.forEach((r) => {
3330
+ const personName = r.person_id ? personNames.get(r.person_id) : void 0;
3331
+ entities.push(indexMembershipRecord(r, personName));
3332
+ });
3333
+ data.homeImprovements?.forEach((i) => {
3334
+ const propertyName = i.property_id ? propertyNames.get(i.property_id) : void 0;
3335
+ entities.push(indexHomeImprovement(i, propertyName));
3336
+ });
3337
+ data.taxYears?.forEach((t) => {
3338
+ const filerNames = t.filer_ids?.map((id) => personNames.get(id)).filter(Boolean);
3339
+ entities.push(indexTaxYear(t, filerNames));
3340
+ });
3341
+ return entities;
3342
+ }
3343
+
3344
+ // ../list-data/src/adapters/contactAdapter.ts
3345
+ function getContactComputedFields(contact) {
3346
+ const computed = {
3347
+ displayName: getContactDisplayName(contact)
3348
+ };
3349
+ if (contact.type) {
3350
+ computed.formattedType = contact.type.charAt(0).toUpperCase() + contact.type.slice(1).replace(/_/g, " ");
3351
+ }
3352
+ return computed;
3353
+ }
3354
+
3355
+ // ../list-data/src/adapters/insuranceAdapter.ts
3356
+ function getInsuranceComputedFields(policy) {
3357
+ const computed = {
3358
+ displayName: policy.provider || "Insurance Policy"
3359
+ };
3360
+ if (policy.expiration_date) {
3361
+ computed.daysUntilExpiry = daysUntil(policy.expiration_date);
3362
+ computed.expiryStatus = getExpiryStatus(computed.daysUntilExpiry);
3363
+ }
3364
+ if (policy.type) {
3365
+ computed.formattedType = policy.type.charAt(0).toUpperCase() + policy.type.slice(1);
3366
+ }
3367
+ return computed;
3368
+ }
3369
+
3370
+ // ../list-data/src/adapters/petAdapter.ts
3371
+ function getPetComputedFields(pet) {
3372
+ const computed = {
3373
+ displayName: pet.name
3374
+ };
3375
+ const detailedAge = calculateDetailedAge(pet.date_of_birth);
3376
+ if (detailedAge !== null && !pet.memorialized_at) {
3377
+ if (detailedAge.years < 1) {
3378
+ const months = detailedAge.months || 1;
3379
+ computed.age = `${months} month${months !== 1 ? "s" : ""}`;
3380
+ } else {
3381
+ computed.age = `${detailedAge.years} year${detailedAge.years !== 1 ? "s" : ""}`;
3382
+ }
3383
+ if (pet.species === "cat") {
3384
+ computed.humanYears = calculateCatHumanYears(detailedAge.fractionalYears);
3385
+ } else if (pet.species === "dog") {
3386
+ let size = "medium";
3387
+ const weightHistory = pet.weight_history || [];
3388
+ const latestWeight = weightHistory.length > 0 ? [...weightHistory].sort((a, b) => b.date.localeCompare(a.date))[0] : null;
3389
+ const weightInLbs = latestWeight ? latestWeight.unit === "kg" ? latestWeight.weight * 2.20462 : latestWeight.weight : pet.weight_lbs;
3390
+ if (weightInLbs) {
3391
+ if (weightInLbs < 20) size = "small";
3392
+ else if (weightInLbs > 50) size = "large";
3393
+ }
3394
+ computed.humanYears = calculateDogHumanYears(detailedAge.fractionalYears, size);
3395
+ }
3396
+ }
3397
+ if (pet.species) {
3398
+ computed.formattedType = pet.species.charAt(0).toUpperCase() + pet.species.slice(1).replace(/_/g, " ");
3399
+ }
3400
+ return computed;
3401
+ }
3402
+
3403
+ // ../list-data/src/adapters/vehicleAdapter.ts
3404
+ function getVehicleComputedFields(vehicle) {
3405
+ const parts = [vehicle.year, vehicle.make, vehicle.model].filter(Boolean);
3406
+ const displayName = parts.length > 0 ? parts.join(" ") : vehicle.name || "Vehicle";
3407
+ const computed = {
3408
+ displayName
3409
+ };
3410
+ if (vehicle.registration_expiration) {
3411
+ const monthNum = parseInt(vehicle.registration_expiration, 10);
3412
+ if (monthNum >= 1 && monthNum <= 12) {
3413
+ const now = /* @__PURE__ */ new Date();
3414
+ const currentYear = now.getFullYear();
3415
+ const currentMonth = now.getMonth() + 1;
3416
+ let targetYear = currentYear;
3417
+ if (monthNum < currentMonth) {
3418
+ targetYear = currentYear + 1;
3419
+ }
3420
+ const expirationDate = new Date(targetYear, monthNum, 0);
3421
+ const daysUntil2 = Math.ceil((expirationDate.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24));
3422
+ computed.daysUntilExpiry = daysUntil2;
3423
+ if (daysUntil2 < 0) {
3424
+ computed.expiryStatus = "expired";
3425
+ } else if (daysUntil2 <= 30) {
3426
+ computed.expiryStatus = "expiring_soon";
3427
+ } else {
3428
+ computed.expiryStatus = "ok";
3429
+ }
3430
+ }
3431
+ }
3432
+ const vehicleType = vehicle.vehicle_type || vehicle.type;
3433
+ if (vehicleType) {
3434
+ computed.formattedType = vehicleType.charAt(0).toUpperCase() + vehicleType.slice(1);
3435
+ }
3436
+ return computed;
3437
+ }
3438
+
3439
+ // ../list-data/src/data/propertyMaintenanceTemplates.json
3440
+ var propertyMaintenanceTemplates_default = [
3441
+ {
3442
+ id: "smoke-detector-maintenance",
3443
+ name: "Smoke & CO Detector Maintenance",
3444
+ description: "Monthly: Test all detectors. Annually: Replace batteries",
3445
+ category: "safety",
3446
+ applicableTo: "property",
3447
+ conditions: {},
3448
+ defaultFrequency: {
3449
+ type: "recurring",
3450
+ interval: "monthly"
3451
+ },
3452
+ priority: "critical",
3453
+ skillLevel: "diy_easy",
3454
+ estimatedTimeMinutes: 15,
3455
+ whyImportant: "Life-saving devices must be functional - test monthly, replace batteries yearly"
3456
+ },
3457
+ {
3458
+ id: "hvac-filter-replacement",
3459
+ name: "Replace HVAC Air Filter",
3460
+ description: "Change air filter in heating/cooling system to maintain efficiency",
3461
+ category: "hvac",
3462
+ applicableTo: "property",
3463
+ conditions: {
3464
+ hasAC: true
3465
+ },
3466
+ defaultFrequency: {
3467
+ type: "recurring",
3468
+ interval: "monthly"
3469
+ },
3470
+ priority: "important",
3471
+ skillLevel: "diy_easy",
3472
+ estimatedTimeMinutes: 10,
3473
+ whyImportant: "Dirty filters reduce efficiency by up to 15% and worsen air quality"
3474
+ },
3475
+ {
3476
+ id: "furnace-inspection",
3477
+ name: "Furnace Inspection & Service",
3478
+ description: "Professional inspection and cleaning of gas furnace",
3479
+ category: "hvac",
3480
+ applicableTo: "property",
3481
+ conditions: {
3482
+ hasGasFurnace: true
3483
+ },
3484
+ defaultFrequency: {
3485
+ type: "seasonal",
3486
+ season: "fall",
3487
+ monthsNorthern: [9],
3488
+ monthsSouthern: [3]
3489
+ },
3490
+ priority: "important",
3491
+ skillLevel: "professional",
3492
+ estimatedTimeMinutes: 90,
3493
+ whyImportant: "Ensures safe operation and prevents carbon monoxide leaks"
3494
+ },
3495
+ {
3496
+ id: "ac-service",
3497
+ name: "Air Conditioning Service",
3498
+ description: "Professional AC inspection and cleaning",
3499
+ category: "hvac",
3500
+ applicableTo: "property",
3501
+ conditions: {
3502
+ hasAC: true
3503
+ },
3504
+ defaultFrequency: {
3505
+ type: "seasonal",
3506
+ season: "spring",
3507
+ monthsNorthern: [4, 5],
3508
+ monthsSouthern: [10, 11]
3509
+ },
3510
+ priority: "important",
3511
+ skillLevel: "professional",
3512
+ estimatedTimeMinutes: 90,
3513
+ whyImportant: "Maintains efficiency and prevents breakdowns in summer heat"
3514
+ },
3515
+ {
3516
+ id: "gutter-cleaning-fall",
3517
+ name: "Clean Gutters (Fall)",
3518
+ description: "Remove leaves and debris from gutters and downspouts",
3519
+ category: "exterior",
3520
+ applicableTo: "property",
3521
+ conditions: {
3522
+ hasGutters: true,
3523
+ propertyType: ["single_family", "townhouse"]
3524
+ },
3525
+ defaultFrequency: {
3526
+ type: "seasonal",
3527
+ season: "fall",
3528
+ monthsNorthern: [10, 11],
3529
+ monthsSouthern: [4, 5]
3530
+ },
3531
+ priority: "important",
3532
+ skillLevel: "diy_moderate",
3533
+ estimatedTimeMinutes: 120,
3534
+ whyImportant: "Clogged gutters can cause water damage to foundation and roof"
3535
+ },
3536
+ {
3537
+ id: "gutter-cleaning-spring",
3538
+ name: "Clean Gutters (Spring)",
3539
+ description: "Remove debris accumulated over winter",
3540
+ category: "exterior",
3541
+ applicableTo: "property",
3542
+ conditions: {
3543
+ hasGutters: true,
3544
+ propertyType: ["single_family", "townhouse"]
3545
+ },
3546
+ defaultFrequency: {
3547
+ type: "seasonal",
3548
+ season: "spring",
3549
+ monthsNorthern: [4, 5],
3550
+ monthsSouthern: [10, 11]
3551
+ },
3552
+ priority: "important",
3553
+ skillLevel: "diy_moderate",
3554
+ estimatedTimeMinutes: 120,
3555
+ whyImportant: "Spring runoff needs clear gutters to prevent water damage"
3556
+ },
3557
+ {
3558
+ id: "roof-moss-removal",
3559
+ name: "Remove Roof Moss",
3560
+ description: "Clean moss and debris from roof shingles",
3561
+ category: "roof",
3562
+ applicableTo: "property",
3563
+ conditions: {
3564
+ propertyType: ["single_family", "townhouse"],
3565
+ climate: ["marine", "humid_subtropical", "humid_continental"]
3566
+ },
3567
+ defaultFrequency: {
3568
+ type: "recurring",
3569
+ interval: "yearly"
3570
+ },
3571
+ priority: "important",
3572
+ skillLevel: "professional",
3573
+ estimatedTimeMinutes: 240,
3574
+ whyImportant: "Moss damages shingles and reduces roof lifespan significantly"
3575
+ },
3576
+ {
3577
+ id: "roof-inspection",
3578
+ name: "Inspect Roof",
3579
+ description: "Check for damaged shingles, leaks, and wear",
3580
+ category: "roof",
3581
+ applicableTo: "property",
3582
+ conditions: {
3583
+ propertyType: ["single_family", "townhouse"]
3584
+ },
3585
+ defaultFrequency: {
3586
+ type: "recurring",
3587
+ interval: "biannually"
3588
+ },
3589
+ priority: "important",
3590
+ skillLevel: "professional",
3591
+ estimatedTimeMinutes: 60,
3592
+ whyImportant: "Early detection of roof damage prevents expensive interior water damage"
3593
+ },
3594
+ {
3595
+ id: "pool-chemistry-check",
3596
+ name: "Check Pool Chemistry",
3597
+ description: "Test and balance pH, chlorine, and alkalinity levels",
3598
+ category: "pool",
3599
+ applicableTo: "property",
3600
+ conditions: {
3601
+ hasPool: true
3602
+ },
3603
+ defaultFrequency: {
3604
+ type: "recurring",
3605
+ interval: "weekly"
3606
+ },
3607
+ priority: "important",
3608
+ skillLevel: "diy_easy",
3609
+ estimatedTimeMinutes: 30,
3610
+ whyImportant: "Prevents algae growth and keeps water safe for swimming"
3611
+ },
3612
+ {
3613
+ id: "pool-filter-cleaning",
3614
+ name: "Clean Pool Filter",
3615
+ description: "Backwash or clean pool filter system",
3616
+ category: "pool",
3617
+ applicableTo: "property",
3618
+ conditions: {
3619
+ hasPool: true
3620
+ },
3621
+ defaultFrequency: {
3622
+ type: "recurring",
3623
+ interval: "monthly"
3624
+ },
3625
+ priority: "important",
3626
+ skillLevel: "diy_moderate",
3627
+ estimatedTimeMinutes: 45,
3628
+ whyImportant: "Keeps water circulation efficient and clear"
3629
+ },
3630
+ {
3631
+ id: "pool-winterization",
3632
+ name: "Winterize Pool",
3633
+ description: "Close pool for winter: drain, add antifreeze, install cover",
3634
+ category: "pool",
3635
+ applicableTo: "property",
3636
+ conditions: {
3637
+ hasPool: true,
3638
+ winterSeverity: ["moderate", "severe"]
3639
+ },
3640
+ defaultFrequency: {
3641
+ type: "seasonal",
3642
+ season: "fall",
3643
+ monthsNorthern: [10],
3644
+ monthsSouthern: [4]
3645
+ },
3646
+ priority: "critical",
3647
+ skillLevel: "professional",
3648
+ estimatedTimeMinutes: 180,
3649
+ whyImportant: "Prevents freeze damage to pool equipment and plumbing"
3650
+ },
3651
+ {
3652
+ id: "pool-opening",
3653
+ name: "Open Pool for Summer",
3654
+ description: "Remove cover, refill, balance chemicals, start equipment",
3655
+ category: "pool",
3656
+ applicableTo: "property",
3657
+ conditions: {
3658
+ hasPool: true,
3659
+ winterSeverity: ["moderate", "severe"]
3660
+ },
3661
+ defaultFrequency: {
3662
+ type: "seasonal",
3663
+ season: "spring",
3664
+ monthsNorthern: [4, 5],
3665
+ monthsSouthern: [10, 11]
3666
+ },
3667
+ priority: "important",
3668
+ skillLevel: "professional",
3669
+ estimatedTimeMinutes: 180,
3670
+ whyImportant: "Proper opening prevents damage and ensures safe swimming"
3671
+ },
3672
+ {
3673
+ id: "sprinkler-system-blowout",
3674
+ name: "Blow Out Sprinkler System",
3675
+ description: "Clear water from sprinkler lines to prevent freeze damage",
3676
+ category: "landscaping",
3677
+ applicableTo: "property",
3678
+ conditions: {
3679
+ hasSprinklerSystem: true,
3680
+ winterSeverity: ["moderate", "severe"]
3681
+ },
3682
+ defaultFrequency: {
3683
+ type: "seasonal",
3684
+ season: "fall",
3685
+ monthsNorthern: [10],
3686
+ monthsSouthern: [4]
3687
+ },
3688
+ priority: "critical",
3689
+ skillLevel: "professional",
3690
+ estimatedTimeMinutes: 60,
3691
+ whyImportant: "Frozen water in lines can cause $1000+ in repairs"
3692
+ },
3693
+ {
3694
+ id: "sprinkler-system-startup",
3695
+ name: "Start Up Sprinkler System",
3696
+ description: "Turn on system, check for leaks, adjust spray patterns",
3697
+ category: "landscaping",
3698
+ applicableTo: "property",
3699
+ conditions: {
3700
+ hasSprinklerSystem: true,
3701
+ winterSeverity: ["moderate", "severe"]
3702
+ },
3703
+ defaultFrequency: {
3704
+ type: "seasonal",
3705
+ season: "spring",
3706
+ monthsNorthern: [4, 5],
3707
+ monthsSouthern: [10, 11]
3708
+ },
3709
+ priority: "important",
3710
+ skillLevel: "diy_moderate",
3711
+ estimatedTimeMinutes: 90,
3712
+ whyImportant: "Catch leaks early before they waste water and money"
3713
+ },
3714
+ {
3715
+ id: "chimney-cleaning-inspection",
3716
+ name: "Chimney Cleaning & Inspection",
3717
+ description: "Remove creosote buildup and inspect for damage",
3718
+ category: "safety",
3719
+ applicableTo: "property",
3720
+ conditions: {
3721
+ hasFireplace: true
3722
+ },
3723
+ defaultFrequency: {
3724
+ type: "recurring",
3725
+ interval: "yearly"
3726
+ },
3727
+ priority: "important",
3728
+ skillLevel: "professional",
3729
+ estimatedTimeMinutes: 120,
3730
+ whyImportant: "Prevents chimney fires caused by creosote buildup"
3731
+ },
3732
+ {
3733
+ id: "septic-tank-pumping",
3734
+ name: "Pump Septic Tank",
3735
+ description: "Professional septic tank pumping and inspection",
3736
+ category: "plumbing",
3737
+ applicableTo: "property",
3738
+ conditions: {
3739
+ propertyType: ["single_family"],
3740
+ hasSepticSystem: true
3741
+ },
3742
+ defaultFrequency: {
3743
+ type: "recurring",
3744
+ customMonths: 36
3745
+ },
3746
+ priority: "critical",
3747
+ skillLevel: "professional",
3748
+ estimatedTimeMinutes: 120,
3749
+ whyImportant: "Prevents backup and expensive drain field replacement ($10,000+)"
3750
+ },
3751
+ {
3752
+ id: "water-heater-flush",
3753
+ name: "Flush Water Heater",
3754
+ description: "Drain sediment from water heater tank",
3755
+ category: "plumbing",
3756
+ applicableTo: "property",
3757
+ conditions: {},
3758
+ defaultFrequency: {
3759
+ type: "recurring",
3760
+ interval: "yearly"
3761
+ },
3762
+ priority: "routine",
3763
+ skillLevel: "diy_moderate",
3764
+ estimatedTimeMinutes: 60,
3765
+ whyImportant: "Removes sediment buildup that reduces efficiency and lifespan"
3766
+ },
3767
+ {
3768
+ id: "deck-staining",
3769
+ name: "Stain/Seal Deck",
3770
+ description: "Apply protective stain or sealant to wood deck",
3771
+ category: "exterior",
3772
+ applicableTo: "property",
3773
+ conditions: {
3774
+ hasDeck: true,
3775
+ deckMaterial: ["wood"]
3776
+ },
3777
+ defaultFrequency: {
3778
+ type: "recurring",
3779
+ customMonths: 24
3780
+ },
3781
+ priority: "important",
3782
+ skillLevel: "diy_moderate",
3783
+ estimatedTimeMinutes: 480,
3784
+ whyImportant: "Protects wood from rot and extends deck life by years"
3785
+ },
3786
+ {
3787
+ id: "driveway-sealing",
3788
+ name: "Seal Asphalt Driveway",
3789
+ description: "Apply sealcoat to protect asphalt surface",
3790
+ category: "exterior",
3791
+ applicableTo: "property",
3792
+ conditions: {
3793
+ hasDriveway: true,
3794
+ drivewayMaterial: ["asphalt"]
3795
+ },
3796
+ defaultFrequency: {
3797
+ type: "recurring",
3798
+ customMonths: 24
3799
+ },
3800
+ priority: "routine",
3801
+ skillLevel: "diy_moderate",
3802
+ estimatedTimeMinutes: 360,
3803
+ whyImportant: "Prevents cracks and extends driveway life significantly"
3804
+ },
3805
+ {
3806
+ id: "power-wash-exterior",
3807
+ name: "Power Wash Home Exterior",
3808
+ description: "Clean siding, walkways, and deck to remove dirt and mildew",
3809
+ category: "exterior",
3810
+ applicableTo: "property",
3811
+ conditions: {
3812
+ propertyType: ["single_family", "townhouse"]
3813
+ },
3814
+ defaultFrequency: {
3815
+ type: "recurring",
3816
+ interval: "yearly"
3817
+ },
3818
+ priority: "routine",
3819
+ skillLevel: "diy_moderate",
3820
+ estimatedTimeMinutes: 240,
3821
+ whyImportant: "Prevents mildew damage and maintains curb appeal"
3822
+ },
3823
+ {
3824
+ id: "window-caulking",
3825
+ name: "Inspect & Recaulk Windows",
3826
+ description: "Check window caulking and reapply where needed",
3827
+ category: "exterior",
3828
+ applicableTo: "property",
3829
+ conditions: {
3830
+ propertyType: ["single_family", "townhouse"]
3831
+ },
3832
+ defaultFrequency: {
3833
+ type: "recurring",
3834
+ customMonths: 24
3835
+ },
3836
+ priority: "routine",
3837
+ skillLevel: "diy_moderate",
3838
+ estimatedTimeMinutes: 180,
3839
+ whyImportant: "Prevents water infiltration and improves energy efficiency"
3840
+ },
3841
+ {
3842
+ id: "dryer-vent-cleaning",
3843
+ name: "Clean Dryer Vent",
3844
+ description: "Remove lint buildup from dryer vent ductwork",
3845
+ category: "appliances",
3846
+ applicableTo: "property",
3847
+ conditions: {},
3848
+ defaultFrequency: {
3849
+ type: "recurring",
3850
+ interval: "yearly"
3851
+ },
3852
+ priority: "important",
3853
+ skillLevel: "diy_moderate",
3854
+ estimatedTimeMinutes: 45,
3855
+ whyImportant: "Prevents fire hazard and improves dryer efficiency"
3856
+ },
3857
+ {
3858
+ id: "refrigerator-coil-cleaning",
3859
+ name: "Clean Refrigerator Coils",
3860
+ description: "Vacuum dust from refrigerator condenser coils",
3861
+ category: "appliances",
3862
+ applicableTo: "property",
3863
+ conditions: {},
3864
+ defaultFrequency: {
3865
+ type: "recurring",
3866
+ interval: "biannually"
3867
+ },
3868
+ priority: "routine",
3869
+ skillLevel: "diy_easy",
3870
+ estimatedTimeMinutes: 30,
3871
+ whyImportant: "Improves efficiency and extends refrigerator lifespan"
3872
+ },
3873
+ {
3874
+ id: "lawn-mowing",
3875
+ name: "Mow Lawn",
3876
+ description: "Cut grass to maintain healthy lawn appearance",
3877
+ category: "landscaping",
3878
+ applicableTo: "property",
3879
+ conditions: {
3880
+ propertyType: ["single_family", "townhouse"],
3881
+ lawnSize: ["small", "medium"]
3882
+ },
3883
+ defaultFrequency: {
3884
+ type: "recurring",
3885
+ interval: "weekly"
3886
+ },
3887
+ priority: "routine",
3888
+ skillLevel: "diy_easy",
3889
+ estimatedTimeMinutes: 60,
3890
+ whyImportant: "Keeps lawn healthy and property looking well-maintained"
3891
+ },
3892
+ {
3893
+ id: "lawn-fertilizing-spring",
3894
+ name: "Fertilize Lawn (Spring)",
3895
+ description: "Apply spring fertilizer to promote growth",
3896
+ category: "landscaping",
3897
+ applicableTo: "property",
3898
+ conditions: {
3899
+ propertyType: ["single_family", "townhouse"],
3900
+ lawnSize: ["small", "medium"]
3901
+ },
3902
+ defaultFrequency: {
3903
+ type: "seasonal",
3904
+ season: "spring",
3905
+ monthsNorthern: [4, 5],
3906
+ monthsSouthern: [10, 11]
3907
+ },
3908
+ priority: "routine",
3909
+ skillLevel: "diy_easy",
3910
+ estimatedTimeMinutes: 90,
3911
+ whyImportant: "Promotes healthy growth and green color"
3912
+ },
3913
+ {
3914
+ id: "lawn-fertilizing-fall",
3915
+ name: "Fertilize Lawn (Fall)",
3916
+ description: "Apply fall fertilizer to strengthen roots",
3917
+ category: "landscaping",
3918
+ applicableTo: "property",
3919
+ conditions: {
3920
+ propertyType: ["single_family", "townhouse"],
3921
+ lawnSize: ["small", "medium"]
3922
+ },
3923
+ defaultFrequency: {
3924
+ type: "seasonal",
3925
+ season: "fall",
3926
+ monthsNorthern: [9, 10],
3927
+ monthsSouthern: [3, 4]
3928
+ },
3929
+ priority: "routine",
3930
+ skillLevel: "diy_easy",
3931
+ estimatedTimeMinutes: 90,
3932
+ whyImportant: "Strengthens roots for winter and early spring growth"
3933
+ },
3934
+ {
3935
+ id: "lawn-aeration",
3936
+ name: "Aerate Lawn",
3937
+ description: "Core aerate lawn to improve water and nutrient absorption",
3938
+ category: "landscaping",
3939
+ applicableTo: "property",
3940
+ conditions: {
3941
+ propertyType: ["single_family", "townhouse"],
3942
+ lawnSize: ["medium"]
3943
+ },
3944
+ defaultFrequency: {
3945
+ type: "recurring",
3946
+ interval: "yearly"
3947
+ },
3948
+ priority: "routine",
3949
+ skillLevel: "diy_moderate",
3950
+ estimatedTimeMinutes: 120,
3951
+ whyImportant: "Reduces soil compaction and promotes healthier grass"
3952
+ },
3953
+ {
3954
+ id: "tree-trimming",
3955
+ name: "Trim Trees & Shrubs",
3956
+ description: "Prune dead branches and shape trees/shrubs",
3957
+ category: "landscaping",
3958
+ applicableTo: "property",
3959
+ conditions: {
3960
+ propertyType: ["single_family", "townhouse"],
3961
+ treeCount: ["few"]
3962
+ },
3963
+ defaultFrequency: {
3964
+ type: "recurring",
3965
+ interval: "yearly"
3966
+ },
3967
+ priority: "routine",
3968
+ skillLevel: "professional",
3969
+ estimatedTimeMinutes: 180,
3970
+ whyImportant: "Prevents damage from falling branches and promotes healthy growth"
3971
+ },
3972
+ {
3973
+ id: "well-pump-testing",
3974
+ name: "Test Well Pump",
3975
+ description: "Check well pump operation and water quality",
3976
+ category: "plumbing",
3977
+ applicableTo: "property",
3978
+ conditions: {
3979
+ hasWell: true
3980
+ },
3981
+ defaultFrequency: {
3982
+ type: "recurring",
3983
+ interval: "yearly"
3984
+ },
3985
+ priority: "important",
3986
+ skillLevel: "professional",
3987
+ estimatedTimeMinutes: 90,
3988
+ whyImportant: "Catches pump issues early before complete failure"
3989
+ },
3990
+ {
3991
+ id: "basement-sump-pump-test",
3992
+ name: "Test Sump Pump",
3993
+ description: "Pour water into sump pit to ensure pump activates properly",
3994
+ category: "plumbing",
3995
+ applicableTo: "property",
3996
+ conditions: {
3997
+ hasBasement: true
3998
+ },
3999
+ defaultFrequency: {
4000
+ type: "recurring",
4001
+ interval: "quarterly"
4002
+ },
4003
+ priority: "important",
4004
+ skillLevel: "diy_easy",
4005
+ estimatedTimeMinutes: 15,
4006
+ whyImportant: "Prevents basement flooding during heavy rain"
4007
+ },
4008
+ {
4009
+ id: "solar-panel-cleaning",
4010
+ name: "Clean Solar Panels",
4011
+ description: "Remove dirt, pollen, bird droppings, and debris from solar panels",
4012
+ category: "solar",
4013
+ applicableTo: "property",
4014
+ conditions: {
4015
+ hasSolarPanels: true
4016
+ },
4017
+ defaultFrequency: {
4018
+ type: "recurring",
4019
+ interval: "yearly"
4020
+ },
4021
+ priority: "important",
4022
+ skillLevel: "diy_moderate",
4023
+ estimatedTimeMinutes: 60,
4024
+ whyImportant: "Dirty panels can reduce energy output by 20-25%"
4025
+ },
4026
+ {
4027
+ id: "solar-panel-visual-inspection",
4028
+ name: "Inspect Solar Panels",
4029
+ description: "Check panels for cracks, hotspots, discoloration, loose connections, and debris buildup",
4030
+ category: "solar",
4031
+ applicableTo: "property",
4032
+ conditions: {
4033
+ hasSolarPanels: true
4034
+ },
4035
+ defaultFrequency: {
4036
+ type: "recurring",
4037
+ interval: "biannually"
4038
+ },
4039
+ priority: "important",
4040
+ skillLevel: "diy_easy",
4041
+ estimatedTimeMinutes: 30,
4042
+ whyImportant: "Early detection of damage prevents costly repairs and output loss"
4043
+ },
4044
+ {
4045
+ id: "solar-inverter-inspection",
4046
+ name: "Check Solar Inverter",
4047
+ description: "Verify inverter is functioning properly, check status lights and error codes",
4048
+ category: "solar",
4049
+ applicableTo: "property",
4050
+ conditions: {
4051
+ hasSolarPanels: true
4052
+ },
4053
+ defaultFrequency: {
4054
+ type: "recurring",
4055
+ interval: "monthly"
4056
+ },
4057
+ priority: "important",
4058
+ skillLevel: "diy_easy",
4059
+ estimatedTimeMinutes: 10,
4060
+ whyImportant: "Inverter failure stops all energy production - catch issues early"
4061
+ },
4062
+ {
4063
+ id: "solar-system-professional-inspection",
4064
+ name: "Professional Solar System Inspection",
4065
+ description: "Full system inspection including wiring, connections, mounting, and performance analysis",
4066
+ category: "solar",
4067
+ applicableTo: "property",
4068
+ conditions: {
4069
+ hasSolarPanels: true
4070
+ },
4071
+ defaultFrequency: {
4072
+ type: "recurring",
4073
+ customMonths: 24
4074
+ },
4075
+ priority: "important",
4076
+ skillLevel: "professional",
4077
+ estimatedTimeMinutes: 120,
4078
+ whyImportant: "Ensures system operates at peak efficiency and maintains warranty"
4079
+ },
4080
+ {
4081
+ id: "home-battery-visual-inspection",
4082
+ name: "Inspect Home Battery System",
4083
+ description: "Check battery unit for damage, leaks, unusual sounds, and ensure ventilation is clear",
4084
+ category: "solar",
4085
+ applicableTo: "property",
4086
+ conditions: {
4087
+ hasHomeBattery: true
4088
+ },
4089
+ defaultFrequency: {
4090
+ type: "recurring",
4091
+ interval: "quarterly"
4092
+ },
4093
+ priority: "important",
4094
+ skillLevel: "diy_easy",
4095
+ estimatedTimeMinutes: 15,
4096
+ whyImportant: "Early detection of battery issues prevents safety hazards and costly damage"
4097
+ },
4098
+ {
4099
+ id: "home-battery-firmware-update",
4100
+ name: "Check Battery Firmware Updates",
4101
+ description: "Check manufacturer app for firmware updates and apply if available",
4102
+ category: "solar",
4103
+ applicableTo: "property",
4104
+ conditions: {
4105
+ hasHomeBattery: true
4106
+ },
4107
+ defaultFrequency: {
4108
+ type: "recurring",
4109
+ interval: "quarterly"
4110
+ },
4111
+ priority: "routine",
4112
+ skillLevel: "diy_easy",
4113
+ estimatedTimeMinutes: 15,
4114
+ whyImportant: "Updates improve performance, fix bugs, and enhance safety features"
4115
+ },
4116
+ {
4117
+ id: "home-battery-professional-inspection",
4118
+ name: "Professional Battery System Inspection",
4119
+ description: "Full inspection of battery connections, performance testing, and state of health analysis",
4120
+ category: "solar",
4121
+ applicableTo: "property",
4122
+ conditions: {
4123
+ hasHomeBattery: true
4124
+ },
4125
+ defaultFrequency: {
4126
+ type: "recurring",
4127
+ interval: "yearly"
4128
+ },
4129
+ priority: "important",
4130
+ skillLevel: "professional",
4131
+ estimatedTimeMinutes: 90,
4132
+ whyImportant: "Ensures safe operation and optimal battery health for longevity"
4133
+ },
4134
+ {
4135
+ id: "window-cleaning",
4136
+ name: "Clean Windows",
4137
+ description: "Clean interior and exterior windows for improved visibility and appearance",
4138
+ category: "interior",
4139
+ applicableTo: "property",
4140
+ conditions: {},
4141
+ defaultFrequency: {
4142
+ type: "recurring",
4143
+ interval: "biannually"
4144
+ },
4145
+ priority: "routine",
4146
+ skillLevel: "diy_moderate",
4147
+ estimatedTimeMinutes: 180,
4148
+ whyImportant: "Clean windows improve natural light and curb appeal"
4149
+ },
4150
+ {
4151
+ id: "carpet-cleaning",
4152
+ name: "Carpet Cleaning",
4153
+ description: "Deep clean carpets to remove dirt, stains, and allergens",
4154
+ category: "interior",
4155
+ applicableTo: "property",
4156
+ conditions: {
4157
+ hasCarpets: true
4158
+ },
4159
+ defaultFrequency: {
4160
+ type: "recurring",
4161
+ interval: "yearly"
4162
+ },
4163
+ priority: "routine",
4164
+ skillLevel: "professional",
4165
+ estimatedTimeMinutes: 240,
4166
+ whyImportant: "Extends carpet life, improves air quality, and removes allergens"
4167
+ }
4168
+ ];
4169
+
4170
+ // ../list-data/src/data/vehicleMaintenanceTemplates.json
4171
+ var vehicleMaintenanceTemplates_default = [
4172
+ {
4173
+ id: "oil-change",
4174
+ name: "Oil Change",
4175
+ description: "Change engine oil and oil filter",
4176
+ category: "engine",
4177
+ applicableTo: "vehicle",
4178
+ conditions: {
4179
+ vehicleType: ["car", "rv"],
4180
+ engineType: ["gasoline", "diesel", "hybrid", "plugin_hybrid"]
4181
+ },
4182
+ defaultFrequency: {
4183
+ type: "recurring",
4184
+ interval: "quarterly"
4185
+ },
4186
+ priority: "critical",
4187
+ skillLevel: "diy_moderate",
4188
+ estimatedTimeMinutes: 45,
4189
+ whyImportant: "Regular oil changes prevent engine wear and extend vehicle life. Most critical maintenance task."
4190
+ },
4191
+ {
4192
+ id: "tire-rotation",
4193
+ name: "Tire Rotation",
4194
+ description: "Rotate tires to ensure even wear",
4195
+ category: "tires",
4196
+ applicableTo: "vehicle",
4197
+ conditions: {
4198
+ vehicleType: ["car", "rv"]
4199
+ },
4200
+ defaultFrequency: {
4201
+ type: "recurring",
4202
+ interval: "biannually"
4203
+ },
4204
+ priority: "important",
4205
+ skillLevel: "diy_moderate",
4206
+ estimatedTimeMinutes: 60,
4207
+ whyImportant: "Even tire wear extends tire life by up to 20% and improves safety and fuel efficiency."
4208
+ },
4209
+ {
4210
+ id: "brake-inspection",
4211
+ name: "Brake Inspection",
4212
+ description: "Inspect brake pads, rotors, and fluid",
4213
+ category: "brakes",
4214
+ applicableTo: "vehicle",
4215
+ conditions: {
4216
+ vehicleType: ["car", "motorcycle", "rv"]
4217
+ },
4218
+ defaultFrequency: {
4219
+ type: "recurring",
4220
+ interval: "biannually"
4221
+ },
4222
+ priority: "critical",
4223
+ skillLevel: "professional",
4224
+ estimatedTimeMinutes: 45,
4225
+ whyImportant: "Brake failure is life-threatening. Regular inspection prevents accidents and costly repairs."
4226
+ },
4227
+ {
4228
+ id: "air-filter-replacement",
4229
+ name: "Replace Engine Air Filter",
4230
+ description: "Replace engine air filter for optimal performance",
4231
+ category: "engine",
4232
+ applicableTo: "vehicle",
4233
+ conditions: {
4234
+ vehicleType: ["car", "rv"],
4235
+ engineType: ["gasoline", "diesel", "hybrid", "plugin_hybrid"]
4236
+ },
4237
+ defaultFrequency: {
4238
+ type: "recurring",
4239
+ interval: "yearly"
4240
+ },
4241
+ priority: "routine",
4242
+ skillLevel: "diy_easy",
4243
+ estimatedTimeMinutes: 15,
4244
+ whyImportant: "Clean air filter improves fuel economy by up to 10% and engine performance."
4245
+ },
4246
+ {
4247
+ id: "cabin-air-filter",
4248
+ name: "Replace Cabin Air Filter",
4249
+ description: "Replace cabin air filter for clean interior air",
4250
+ category: "interior",
4251
+ applicableTo: "vehicle",
4252
+ conditions: {
4253
+ vehicleType: ["car", "rv"]
4254
+ },
4255
+ defaultFrequency: {
4256
+ type: "recurring",
4257
+ interval: "yearly"
4258
+ },
4259
+ priority: "routine",
4260
+ skillLevel: "diy_easy",
4261
+ estimatedTimeMinutes: 10,
4262
+ whyImportant: "Filters out pollen, dust, and pollutants for healthier cabin air."
4263
+ },
4264
+ {
4265
+ id: "battery-check",
4266
+ name: "Battery Test",
4267
+ description: "Test battery voltage and terminals, clean corrosion",
4268
+ category: "electrical",
4269
+ applicableTo: "vehicle",
4270
+ conditions: {
4271
+ vehicleType: ["car", "rv"]
4272
+ },
4273
+ defaultFrequency: {
4274
+ type: "recurring",
4275
+ interval: "biannually"
4276
+ },
4277
+ priority: "important",
4278
+ skillLevel: "diy_easy",
4279
+ estimatedTimeMinutes: 20,
4280
+ whyImportant: "Prevents unexpected breakdowns. Most batteries last 3-5 years."
4281
+ },
4282
+ {
4283
+ id: "coolant-flush",
4284
+ name: "Coolant Flush",
4285
+ description: "Drain and replace engine coolant/antifreeze",
4286
+ category: "engine",
4287
+ applicableTo: "vehicle",
4288
+ conditions: {
4289
+ vehicleType: ["car", "rv"],
4290
+ engineType: ["gasoline", "diesel", "hybrid", "plugin_hybrid"]
4291
+ },
4292
+ defaultFrequency: {
4293
+ type: "recurring",
4294
+ interval: "biannually"
4295
+ },
4296
+ priority: "important",
4297
+ skillLevel: "diy_moderate",
4298
+ estimatedTimeMinutes: 90,
4299
+ whyImportant: "Prevents engine overheating and corrosion. Critical for engine longevity."
4300
+ },
4301
+ {
4302
+ id: "transmission-service",
4303
+ name: "Transmission Service",
4304
+ description: "Change transmission fluid and filter",
4305
+ category: "drivetrain",
4306
+ applicableTo: "vehicle",
4307
+ conditions: {
4308
+ vehicleType: ["car", "rv"],
4309
+ transmissionType: ["automatic", "cvt"]
4310
+ },
4311
+ defaultFrequency: {
4312
+ type: "recurring",
4313
+ interval: "biannually"
4314
+ },
4315
+ priority: "important",
4316
+ skillLevel: "professional",
4317
+ estimatedTimeMinutes: 120,
4318
+ whyImportant: "Extends transmission life. Transmission replacement can cost $3,000-$8,000."
4319
+ },
4320
+ {
4321
+ id: "spark-plugs",
4322
+ name: "Replace Spark Plugs",
4323
+ description: "Replace spark plugs for optimal ignition",
4324
+ category: "engine",
4325
+ applicableTo: "vehicle",
4326
+ conditions: {
4327
+ vehicleType: ["car", "rv"],
4328
+ engineType: ["gasoline", "hybrid", "plugin_hybrid"]
4329
+ },
4330
+ defaultFrequency: {
4331
+ type: "recurring",
4332
+ interval: "yearly"
4333
+ },
4334
+ priority: "routine",
4335
+ skillLevel: "diy_moderate",
4336
+ estimatedTimeMinutes: 60,
4337
+ whyImportant: "Improves fuel efficiency, reduces emissions, and ensures smooth engine operation."
4338
+ },
4339
+ {
4340
+ id: "wiper-blades",
4341
+ name: "Replace Wiper Blades",
4342
+ description: "Replace windshield wiper blades",
4343
+ category: "exterior",
4344
+ applicableTo: "vehicle",
4345
+ conditions: {
4346
+ vehicleType: ["car", "rv"]
4347
+ },
4348
+ defaultFrequency: {
4349
+ type: "recurring",
4350
+ interval: "yearly"
4351
+ },
4352
+ priority: "routine",
4353
+ skillLevel: "diy_easy",
4354
+ estimatedTimeMinutes: 10,
4355
+ whyImportant: "Essential for visibility and safety in rain and snow."
4356
+ },
4357
+ {
4358
+ id: "wheel-alignment",
4359
+ name: "Wheel Alignment",
4360
+ description: "Check and adjust wheel alignment",
4361
+ category: "tires",
4362
+ applicableTo: "vehicle",
4363
+ conditions: {
4364
+ vehicleType: ["car"]
4365
+ },
4366
+ defaultFrequency: {
4367
+ type: "recurring",
4368
+ interval: "yearly"
4369
+ },
4370
+ priority: "routine",
4371
+ skillLevel: "professional",
4372
+ estimatedTimeMinutes: 60,
4373
+ whyImportant: "Prevents uneven tire wear and improves handling and fuel economy."
4374
+ },
4375
+ {
4376
+ id: "timing-belt",
4377
+ name: "Timing Belt Replacement",
4378
+ description: "Replace timing belt (if applicable)",
4379
+ category: "engine",
4380
+ applicableTo: "vehicle",
4381
+ conditions: {
4382
+ vehicleType: ["car"],
4383
+ engineType: ["gasoline", "diesel"]
4384
+ },
4385
+ defaultFrequency: {
4386
+ type: "recurring",
4387
+ interval: "yearly"
4388
+ },
4389
+ priority: "critical",
4390
+ skillLevel: "professional",
4391
+ estimatedTimeMinutes: 240,
4392
+ whyImportant: "Timing belt failure can cause catastrophic engine damage. Check your owner's manual for mileage interval (typically 60k-100k miles)."
4393
+ },
4394
+ {
4395
+ id: "differential-service",
4396
+ name: "Differential Service",
4397
+ description: "Change differential fluid",
4398
+ category: "drivetrain",
4399
+ applicableTo: "vehicle",
4400
+ conditions: {
4401
+ vehicleType: ["car"],
4402
+ driveType: ["awd", "4wd", "rwd"]
4403
+ },
4404
+ defaultFrequency: {
4405
+ type: "recurring",
4406
+ interval: "biannually"
4407
+ },
4408
+ priority: "routine",
4409
+ skillLevel: "professional",
4410
+ estimatedTimeMinutes: 90,
4411
+ whyImportant: "Maintains drivetrain efficiency and prevents costly differential failure."
4412
+ },
4413
+ {
4414
+ id: "motorcycle-chain-maintenance",
4415
+ name: "Chain Cleaning & Lubrication",
4416
+ description: "Clean and lubricate motorcycle chain",
4417
+ category: "drivetrain",
4418
+ applicableTo: "vehicle",
4419
+ conditions: {
4420
+ vehicleType: ["motorcycle"]
4421
+ },
4422
+ defaultFrequency: {
4423
+ type: "recurring",
4424
+ interval: "monthly"
4425
+ },
4426
+ priority: "important",
4427
+ skillLevel: "diy_easy",
4428
+ estimatedTimeMinutes: 30,
4429
+ whyImportant: "Prevents chain wear and ensures safe operation. Chain failure can be dangerous."
4430
+ },
4431
+ {
4432
+ id: "bicycle-tune-up",
4433
+ name: "Bicycle Tune-Up",
4434
+ description: "Check brakes, gears, tire pressure, chain lubrication",
4435
+ category: "general",
4436
+ applicableTo: "vehicle",
4437
+ conditions: {
4438
+ vehicleType: ["bicycle"]
4439
+ },
4440
+ defaultFrequency: {
4441
+ type: "recurring",
4442
+ interval: "quarterly"
4443
+ },
4444
+ priority: "important",
4445
+ skillLevel: "diy_easy",
4446
+ estimatedTimeMinutes: 45,
4447
+ whyImportant: "Ensures safe and efficient riding. Prevents accidents and costly repairs."
4448
+ },
4449
+ {
4450
+ id: "ev-battery-check",
4451
+ name: "EV Battery Health Check",
4452
+ description: "Professional battery health diagnostic",
4453
+ category: "electrical",
4454
+ applicableTo: "vehicle",
4455
+ conditions: {
4456
+ vehicleType: ["car"],
4457
+ engineType: ["electric", "plugin_hybrid"]
4458
+ },
4459
+ defaultFrequency: {
4460
+ type: "recurring",
4461
+ interval: "yearly"
4462
+ },
4463
+ priority: "important",
4464
+ skillLevel: "professional",
4465
+ estimatedTimeMinutes: 45,
4466
+ whyImportant: "Monitors battery degradation and ensures warranty compliance."
4467
+ },
4468
+ {
4469
+ id: "winter-tire-swap",
4470
+ name: "Winter Tire Installation",
4471
+ description: "Swap to winter tires for cold weather",
4472
+ category: "tires",
4473
+ applicableTo: "vehicle",
4474
+ conditions: {
4475
+ vehicleType: ["car"],
4476
+ hasWinterTires: true
4477
+ },
4478
+ defaultFrequency: {
4479
+ type: "recurring",
4480
+ interval: "yearly"
4481
+ },
4482
+ priority: "important",
4483
+ skillLevel: "diy_moderate",
4484
+ estimatedTimeMinutes: 90,
4485
+ whyImportant: "Winter tires dramatically improve traction and safety in snow and ice."
4486
+ },
4487
+ {
4488
+ id: "boat-engine-oil",
4489
+ name: "Boat Engine Oil Change",
4490
+ description: "Change engine oil and filter for boat motor",
4491
+ category: "engine",
4492
+ applicableTo: "vehicle",
4493
+ conditions: {
4494
+ vehicleType: ["boat"]
4495
+ },
4496
+ defaultFrequency: {
4497
+ type: "recurring",
4498
+ interval: "yearly"
4499
+ },
4500
+ priority: "critical",
4501
+ skillLevel: "diy_moderate",
4502
+ estimatedTimeMinutes: 60,
4503
+ whyImportant: "Marine engines operate in harsh conditions. Regular oil changes prevent corrosion and extend engine life."
4504
+ },
4505
+ {
4506
+ id: "boat-hull-inspection",
4507
+ name: "Hull Inspection & Cleaning",
4508
+ description: "Inspect hull for damage, clean barnacles and growth",
4509
+ category: "exterior",
4510
+ applicableTo: "vehicle",
4511
+ conditions: {
4512
+ vehicleType: ["boat"]
4513
+ },
4514
+ defaultFrequency: {
4515
+ type: "recurring",
4516
+ interval: "biannually"
4517
+ },
4518
+ priority: "important",
4519
+ skillLevel: "professional",
4520
+ estimatedTimeMinutes: 180,
4521
+ whyImportant: "Hull growth reduces fuel efficiency and speed. Damage can lead to costly repairs or sinking."
4522
+ },
4523
+ {
4524
+ id: "boat-winterization",
4525
+ name: "Winterization",
4526
+ description: "Prepare boat for winter storage: drain water, stabilize fuel, protect engine",
4527
+ category: "general",
4528
+ applicableTo: "vehicle",
4529
+ conditions: {
4530
+ vehicleType: ["boat"]
4531
+ },
4532
+ defaultFrequency: {
4533
+ type: "recurring",
4534
+ interval: "yearly"
4535
+ },
4536
+ priority: "critical",
4537
+ skillLevel: "professional",
4538
+ estimatedTimeMinutes: 240,
4539
+ whyImportant: "Prevents freeze damage to engine and systems. Skipping winterization can cause thousands in repairs."
4540
+ },
4541
+ {
4542
+ id: "boat-battery-maintenance",
4543
+ name: "Battery Check & Charging",
4544
+ description: "Test battery, check terminals, maintain charge",
4545
+ category: "electrical",
4546
+ applicableTo: "vehicle",
4547
+ conditions: {
4548
+ vehicleType: ["boat"]
4549
+ },
4550
+ defaultFrequency: {
4551
+ type: "recurring",
4552
+ interval: "quarterly"
4553
+ },
4554
+ priority: "important",
4555
+ skillLevel: "diy_easy",
4556
+ estimatedTimeMinutes: 30,
4557
+ whyImportant: "Dead battery leaves you stranded on water. Marine batteries need regular charging and maintenance."
4558
+ },
4559
+ {
4560
+ id: "boat-safety-equipment",
4561
+ name: "Safety Equipment Inspection",
4562
+ description: "Check life jackets, fire extinguishers, flares, first aid kit",
4563
+ category: "safety",
4564
+ applicableTo: "vehicle",
4565
+ conditions: {
4566
+ vehicleType: ["boat"]
4567
+ },
4568
+ defaultFrequency: {
4569
+ type: "recurring",
4570
+ interval: "yearly"
4571
+ },
4572
+ priority: "critical",
4573
+ skillLevel: "diy_easy",
4574
+ estimatedTimeMinutes: 45,
4575
+ whyImportant: "Required by law and essential for safety. Expired flares and damaged life jackets can be life-threatening."
4576
+ },
4577
+ {
4578
+ id: "boat-bilge-pump",
4579
+ name: "Bilge Pump Test",
4580
+ description: "Test bilge pump operation and clean intake",
4581
+ category: "safety",
4582
+ applicableTo: "vehicle",
4583
+ conditions: {
4584
+ vehicleType: ["boat"]
4585
+ },
4586
+ defaultFrequency: {
4587
+ type: "recurring",
4588
+ interval: "quarterly"
4589
+ },
4590
+ priority: "critical",
4591
+ skillLevel: "diy_easy",
4592
+ estimatedTimeMinutes: 20,
4593
+ whyImportant: "Bilge pump failure can lead to sinking. Regular testing ensures it works when needed."
4594
+ },
4595
+ {
4596
+ id: "boat-propeller-inspection",
4597
+ name: "Propeller Inspection",
4598
+ description: "Check propeller for damage, dings, fishing line",
4599
+ category: "drivetrain",
4600
+ applicableTo: "vehicle",
4601
+ conditions: {
4602
+ vehicleType: ["boat"]
4603
+ },
4604
+ defaultFrequency: {
4605
+ type: "recurring",
4606
+ interval: "biannually"
4607
+ },
4608
+ priority: "important",
4609
+ skillLevel: "diy_easy",
4610
+ estimatedTimeMinutes: 30,
4611
+ whyImportant: "Damaged props reduce performance and fuel efficiency. Can damage transmission if left unchecked."
4612
+ },
4613
+ {
4614
+ id: "boat-fuel-system",
4615
+ name: "Fuel System Maintenance",
4616
+ description: "Add fuel stabilizer, check fuel lines and water separator",
4617
+ category: "engine",
4618
+ applicableTo: "vehicle",
4619
+ conditions: {
4620
+ vehicleType: ["boat"]
4621
+ },
4622
+ defaultFrequency: {
4623
+ type: "recurring",
4624
+ interval: "yearly"
4625
+ },
4626
+ priority: "important",
4627
+ skillLevel: "diy_moderate",
4628
+ estimatedTimeMinutes: 60,
4629
+ whyImportant: "Marine fuel degrades quickly. Prevents engine damage from bad fuel and water contamination."
4630
+ }
4631
+ ];
4632
+
4633
+ // ../list-data/src/adapters/maintenanceAdapter.ts
4634
+ var ALL_MAINTENANCE_TEMPLATES = [
4635
+ ...propertyMaintenanceTemplates_default,
4636
+ ...vehicleMaintenanceTemplates_default
4637
+ ];
4638
+
4639
+ // ../list-data/src/adapters/subscriptionAdapter.ts
4640
+ function getSubscriptionComputedFields(sub) {
4641
+ const computed = {
4642
+ displayName: sub.name || sub.provider || "Subscription"
4643
+ };
4644
+ if (sub.expiration_date) {
4645
+ computed.daysUntilExpiry = daysUntil(sub.expiration_date);
4646
+ computed.expiryStatus = getExpiryStatus(computed.daysUntilExpiry);
4647
+ }
4648
+ if (sub.category) {
4649
+ computed.formattedType = sub.category.charAt(0).toUpperCase() + sub.category.slice(1).replace(/_/g, " ");
4650
+ }
4651
+ return computed;
4652
+ }
4653
+
4654
+ // ../list-data/src/adapters/credentialAdapter.ts
4655
+ function getCredentialComputedFields(credential) {
4656
+ const computed = {
4657
+ displayName: credential.name || credential.credential_type || "Credential"
4658
+ };
4659
+ if (credential.expiration_date) {
4660
+ computed.daysUntilExpiry = daysUntil(credential.expiration_date);
4661
+ computed.expiryStatus = getExpiryStatus(computed.daysUntilExpiry);
4662
+ }
4663
+ if (credential.credential_type) {
4664
+ computed.formattedType = credential.credential_type.charAt(0).toUpperCase() + credential.credential_type.slice(1).replace(/_/g, " ");
4665
+ }
4666
+ return computed;
4667
+ }
4668
+
4669
+ // src/enrichment.ts
4670
+ function getComputedFields(entity, entityType) {
4671
+ switch (entityType) {
4672
+ case "pet":
4673
+ return getPetComputedFields(entity);
4674
+ case "vehicle":
4675
+ return getVehicleComputedFields(entity);
4676
+ case "insurance":
4677
+ return getInsuranceComputedFields(entity);
4678
+ case "subscription":
4679
+ return getSubscriptionComputedFields(entity);
4680
+ case "contact":
4681
+ return getContactComputedFields(entity);
4682
+ case "credential":
4683
+ return getCredentialComputedFields(entity);
4684
+ default:
4685
+ return {
4686
+ displayName: getEntityDisplayName(entityType, entity)
4687
+ };
4688
+ }
4689
+ }
4690
+ function enrichEntity(entity, entityType) {
4691
+ const computed = getComputedFields(entity, entityType);
4692
+ return { ...entity, _computed: computed };
4693
+ }
4694
+ function enrichEntities(entities, entityType) {
4695
+ return entities.map((entity) => enrichEntity(entity, entityType));
4696
+ }
4697
+
4698
+ // src/resources/entities.ts
4699
+ var ENTITY_TYPES = [
4700
+ "pet",
4701
+ "property",
4702
+ "vehicle",
4703
+ "contact",
4704
+ "insurance",
4705
+ "bank_account",
4706
+ "investment",
4707
+ "subscription",
4708
+ "maintenance_task",
4709
+ "password",
4710
+ "access_code",
4711
+ "document",
4712
+ "medical",
4713
+ "prescription",
4714
+ "credential",
4715
+ "utility"
4716
+ ];
4717
+ async function getEntities(householdId, entityType, privateKey, privacyMode) {
4718
+ const entities = await getDecryptedEntities(householdId, entityType, privateKey);
4719
+ return enrichEntities(entities, entityType).map(
4720
+ (entity) => redactEntity(entity, entityType, privacyMode)
4721
+ );
4722
+ }
4723
+ async function getExpiringItems(days, householdId, privateKey) {
4724
+ const households = await getHouseholds();
4725
+ const searchHouseholds = householdId ? households.filter((h) => h.id === householdId) : households;
4726
+ const now = /* @__PURE__ */ new Date();
4727
+ const cutoff = new Date(now.getTime() + days * 24 * 60 * 60 * 1e3);
4728
+ const expiring = [];
4729
+ for (const household of searchHouseholds) {
4730
+ const insurance = await getDecryptedEntities(household.id, "insurance", privateKey);
4731
+ for (const policy of insurance) {
4732
+ if (policy.expiration_date) {
4733
+ const expires = new Date(policy.expiration_date);
4734
+ if (expires <= cutoff) {
4735
+ expiring.push({
4736
+ householdId: household.id,
4737
+ householdName: household.name,
4738
+ type: "insurance",
4739
+ name: policy.name || policy.provider || policy.policy_number,
4740
+ expiresAt: policy.expiration_date,
4741
+ daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
4742
+ });
4743
+ }
4744
+ }
4745
+ }
4746
+ const vehicles = await getDecryptedEntities(household.id, "vehicle", privateKey);
4747
+ for (const vehicle of vehicles) {
4748
+ const vehicleName = `${vehicle.year || ""} ${vehicle.make || ""} ${vehicle.model || ""}`.trim() || "Vehicle";
4749
+ if (vehicle.registration_expiration) {
4750
+ const expires = new Date(vehicle.registration_expiration);
4751
+ if (expires <= cutoff) {
4752
+ expiring.push({
4753
+ householdId: household.id,
4754
+ householdName: household.name,
4755
+ type: "vehicle_registration",
4756
+ name: vehicleName,
4757
+ expiresAt: vehicle.registration_expiration,
4758
+ daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
4759
+ });
4760
+ }
4761
+ }
4762
+ if (vehicle.tabs_expiration) {
4763
+ const expires = new Date(vehicle.tabs_expiration);
4764
+ if (expires <= cutoff) {
4765
+ expiring.push({
4766
+ householdId: household.id,
4767
+ householdName: household.name,
4768
+ type: "vehicle_tabs",
4769
+ name: vehicleName,
4770
+ expiresAt: vehicle.tabs_expiration,
4771
+ daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
4772
+ });
4773
+ }
4774
+ }
4775
+ }
4776
+ const subscriptions = await getDecryptedEntities(household.id, "subscription", privateKey);
4777
+ for (const sub of subscriptions) {
4778
+ if (sub.expiration_date) {
4779
+ const expires = new Date(sub.expiration_date);
4780
+ if (expires <= cutoff) {
4781
+ expiring.push({
4782
+ householdId: household.id,
4783
+ householdName: household.name,
4784
+ type: "subscription",
4785
+ name: sub.name || sub.custom_name || sub.provider || sub.service_name,
4786
+ expiresAt: sub.expiration_date,
4787
+ daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
4788
+ });
4789
+ }
4790
+ }
4791
+ }
4792
+ const credentials = await getDecryptedEntities(household.id, "credential", privateKey);
4793
+ for (const cred of credentials) {
4794
+ if (cred.expiration_date) {
4795
+ const expires = new Date(cred.expiration_date);
4796
+ if (expires <= cutoff) {
4797
+ expiring.push({
4798
+ householdId: household.id,
4799
+ householdName: household.name,
4800
+ type: "credential",
4801
+ name: cred.name || cred.credential_type,
4802
+ expiresAt: cred.expiration_date,
4803
+ daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
4804
+ });
4805
+ }
4806
+ }
4807
+ }
4808
+ }
4809
+ expiring.sort((a, b) => a.daysUntil - b.daysUntil);
4810
+ return expiring;
4811
+ }
4812
+
4813
+ // src/tools/search.ts
4814
+ async function buildEntitiesData(householdId, privateKey) {
4815
+ const entityTypeMapping = {
4816
+ property: "properties",
4817
+ vehicle: "vehicles",
4818
+ pet: "pets",
4819
+ contact: "contacts",
4820
+ subscription: "subscriptions",
4821
+ service: "services",
4822
+ insurance: "insurancePolicies",
4823
+ valuable: "valuables",
4824
+ bank_account: "financialAccounts",
4825
+ investment: "financialAccounts",
4826
+ credential: "credentials",
4827
+ maintenance_task: "maintenanceTasks",
4828
+ access_code: "accessCodes",
4829
+ device: "devices",
4830
+ person: "people",
4831
+ pet_vet_visit: "petVetVisits",
4832
+ vehicle_service: "vehicleServices",
4833
+ legal: "legalDocuments",
4834
+ health_record: "healthRecords",
4835
+ education_record: "educationRecords",
4836
+ military_record: "militaryRecords",
4837
+ membership_record: "membershipRecords",
4838
+ home_improvement: "homeImprovements",
4839
+ tax_year: "taxYears"
4840
+ };
4841
+ const data = {};
4842
+ const entityTypes = Object.keys(entityTypeMapping);
4843
+ for (const type of entityTypes) {
4844
+ try {
4845
+ const entities = await getDecryptedEntities(householdId, type, privateKey);
4846
+ if (entities.length > 0) {
4847
+ const propName = entityTypeMapping[type];
4848
+ const existing = data[propName] || [];
4849
+ data[propName] = [...existing, ...entities];
4850
+ }
4851
+ } catch {
4852
+ }
4853
+ }
4854
+ return data;
4855
+ }
4856
+ async function executeSearch(params, privateKey, privacyMode) {
4857
+ const { query, householdId, entityType } = params;
4858
+ const households = await getHouseholds();
4859
+ const searchHouseholds = householdId ? households.filter((h) => h.id === householdId) : households;
4860
+ const results = [];
4861
+ for (const household of searchHouseholds) {
4862
+ const entitiesData = await buildEntitiesData(household.id, privateKey);
4863
+ const searchableEntities = indexAllEntities(entitiesData);
4864
+ const searchResults = searchEntities(searchableEntities, query, {
4865
+ maxTotal: 50,
4866
+ includeTypes: entityType ? [entityType] : void 0
4867
+ });
4868
+ for (const result of searchResults) {
4869
+ const type = result.entity.entityType;
4870
+ const entityId = result.entity.id;
4871
+ const entities = await getDecryptedEntities(household.id, type, privateKey);
4872
+ const fullEntity = entities.find((e) => e.id === entityId);
4873
+ if (fullEntity) {
4874
+ const enriched = enrichEntity(fullEntity, type);
4875
+ const redacted = redactEntity(enriched, type, privacyMode);
4876
+ results.push({
4877
+ householdId: household.id,
4878
+ householdName: household.name,
4879
+ entityType: type,
4880
+ entity: redacted,
4881
+ score: result.score,
4882
+ matchedFields: result.matchedFields
4883
+ });
4884
+ }
4885
+ }
4886
+ }
4887
+ results.sort((a, b) => b.score - a.score);
4888
+ return results;
4889
+ }
4890
+
4891
+ // src/tools/files.ts
4892
+ async function executeFileDownload(client, params) {
4893
+ const { fileId, householdId, entityId, entityType } = params;
4894
+ try {
4895
+ const result = await downloadAndDecryptFile(
4896
+ client,
4897
+ householdId,
4898
+ fileId,
4899
+ entityId,
4900
+ entityType
4901
+ );
4902
+ return {
4903
+ success: true,
4904
+ fileName: result.fileName,
4905
+ mimeType: result.mimeType,
4906
+ size: result.bytes.length,
4907
+ data: result.dataBase64
4908
+ };
4909
+ } catch (err) {
4910
+ return {
4911
+ success: false,
4912
+ error: err.message
4913
+ };
4914
+ }
4915
+ }
4916
+
4917
+ // src/server.ts
4918
+ async function startServer(mode) {
4919
+ const config = loadConfig();
4920
+ const privacyMode = mode || config.defaultMode;
4921
+ console.error(`[MCP] Starting server in ${privacyMode} mode`);
4922
+ const client = await getAuthenticatedClient();
4923
+ if (!client) {
4924
+ console.error("[MCP] Not logged in. Run: estatehelm login");
4925
+ process.exit(1);
4926
+ }
4927
+ const privateKey = await getPrivateKey();
4928
+ if (!privateKey) {
4929
+ console.error("[MCP] Failed to load encryption keys. Run: estatehelm login");
4930
+ process.exit(1);
4931
+ }
4932
+ await initCache();
4933
+ console.error("[MCP] Checking for updates...");
4934
+ const synced = await syncIfNeeded(client, privateKey);
4935
+ if (synced) {
4936
+ console.error("[MCP] Cache updated");
4937
+ } else {
4938
+ console.error("[MCP] Cache is up to date");
4939
+ }
4940
+ const server = new import_server.Server(
4941
+ {
4942
+ name: "estatehelm",
4943
+ version: "1.0.0"
4944
+ },
4945
+ {
4946
+ capabilities: {
4947
+ resources: {},
4948
+ tools: {},
4949
+ prompts: {}
4950
+ }
4951
+ }
4952
+ );
4953
+ server.setRequestHandler(import_types42.ListResourcesRequestSchema, async () => {
4954
+ const households = await listHouseholds();
4955
+ const resources = [
4956
+ {
4957
+ uri: "estatehelm://households",
4958
+ name: "All Households",
4959
+ description: "List of all households you have access to",
4960
+ mimeType: "application/json"
4961
+ }
4962
+ ];
4963
+ for (const household of households) {
4964
+ resources.push({
4965
+ uri: `estatehelm://households/${household.id}`,
4966
+ name: household.name,
4967
+ description: `Household: ${household.name}`,
4968
+ mimeType: "application/json"
4969
+ });
4970
+ for (const type of ENTITY_TYPES) {
4971
+ resources.push({
4972
+ uri: `estatehelm://households/${household.id}/${type}`,
4973
+ name: `${household.name} - ${formatEntityType(type)}`,
4974
+ description: `${formatEntityType(type)} in ${household.name}`,
4975
+ mimeType: "application/json"
4976
+ });
4977
+ }
4978
+ }
4979
+ return { resources };
4980
+ });
4981
+ server.setRequestHandler(import_types42.ReadResourceRequestSchema, async (request) => {
4982
+ const uri = request.params.uri;
4983
+ const parsed = parseResourceUri(uri);
4984
+ if (!parsed) {
4985
+ throw new Error(`Invalid resource URI: ${uri}`);
4986
+ }
4987
+ let content;
4988
+ if (parsed.type === "households" && !parsed.householdId) {
4989
+ content = await listHouseholds();
4990
+ } else if (parsed.type === "households" && parsed.householdId && !parsed.entityType) {
4991
+ content = await getHousehold(parsed.householdId);
4992
+ if (!content) {
4993
+ throw new Error(`Household not found: ${parsed.householdId}`);
4994
+ }
4995
+ } else if (parsed.householdId && parsed.entityType) {
4996
+ content = await getEntities(parsed.householdId, parsed.entityType, privateKey, privacyMode);
4997
+ } else {
4998
+ throw new Error(`Unsupported resource: ${uri}`);
4999
+ }
5000
+ return {
5001
+ contents: [
5002
+ {
5003
+ uri,
5004
+ mimeType: "application/json",
5005
+ text: JSON.stringify(content, null, 2)
5006
+ }
5007
+ ]
5008
+ };
5009
+ });
5010
+ server.setRequestHandler(import_types42.ListToolsRequestSchema, async () => {
5011
+ return {
5012
+ tools: [
5013
+ {
5014
+ name: "search_entities",
5015
+ description: "Search across all entities in EstateHelm. Returns results with computed fields (age, days until expiry, etc.)",
5016
+ inputSchema: {
5017
+ type: "object",
5018
+ properties: {
5019
+ query: {
5020
+ type: "string",
5021
+ description: "Search query"
5022
+ },
5023
+ householdId: {
5024
+ type: "string",
5025
+ description: "Optional: Limit search to a specific household"
5026
+ },
5027
+ entityType: {
5028
+ type: "string",
5029
+ description: "Optional: Limit search to a specific entity type"
5030
+ }
5031
+ },
5032
+ required: ["query"]
5033
+ }
5034
+ },
5035
+ {
5036
+ name: "get_household_summary",
5037
+ description: "Get a summary of a household including counts and key dates",
5038
+ inputSchema: {
5039
+ type: "object",
5040
+ properties: {
5041
+ householdId: {
5042
+ type: "string",
5043
+ description: "The household ID"
5044
+ }
5045
+ },
5046
+ required: ["householdId"]
5047
+ }
5048
+ },
5049
+ {
5050
+ name: "get_expiring_items",
5051
+ description: "Get items expiring within a given number of days (insurance, vehicle registration, credentials, subscriptions)",
5052
+ inputSchema: {
5053
+ type: "object",
5054
+ properties: {
5055
+ days: {
2580
5056
  type: "number",
2581
5057
  description: "Number of days to look ahead (default: 30)"
2582
5058
  },
@@ -2587,6 +5063,32 @@ async function startServer(mode) {
2587
5063
  }
2588
5064
  }
2589
5065
  },
5066
+ {
5067
+ name: "get_file",
5068
+ description: "Download and decrypt a file attachment. Returns base64-encoded file data that can be saved to disk.",
5069
+ inputSchema: {
5070
+ type: "object",
5071
+ properties: {
5072
+ fileId: {
5073
+ type: "string",
5074
+ description: "The file ID to download"
5075
+ },
5076
+ householdId: {
5077
+ type: "string",
5078
+ description: "The household ID the file belongs to"
5079
+ },
5080
+ entityId: {
5081
+ type: "string",
5082
+ description: "The entity ID the file is attached to"
5083
+ },
5084
+ entityType: {
5085
+ type: "string",
5086
+ description: "The entity type (e.g., insurance, document)"
5087
+ }
5088
+ },
5089
+ required: ["fileId", "householdId", "entityId", "entityType"]
5090
+ }
5091
+ },
2590
5092
  {
2591
5093
  name: "refresh",
2592
5094
  description: "Force refresh of cached data from the server",
@@ -2598,175 +5100,56 @@ async function startServer(mode) {
2598
5100
  ]
2599
5101
  };
2600
5102
  });
2601
- server.setRequestHandler(import_types5.CallToolRequestSchema, async (request) => {
5103
+ server.setRequestHandler(import_types42.CallToolRequestSchema, async (request) => {
2602
5104
  const { name, arguments: args } = request.params;
2603
5105
  switch (name) {
2604
5106
  case "search_entities": {
2605
- const { query, householdId, entityType } = args;
2606
- const households = await getHouseholds();
2607
- const searchHouseholds = householdId ? households.filter((h) => h.id === householdId) : households;
2608
- const results = [];
2609
- for (const household of searchHouseholds) {
2610
- const entityTypes = entityType ? [entityType] : [
2611
- "pet",
2612
- "property",
2613
- "vehicle",
2614
- "contact",
2615
- "insurance",
2616
- "bank_account",
2617
- "investment",
2618
- "subscription",
2619
- "maintenance_task",
2620
- "password",
2621
- "access_code"
2622
- ];
2623
- for (const type of entityTypes) {
2624
- const entities = await getDecryptedEntities(household.id, type, privateKey);
2625
- const matches = entities.filter((e) => searchEntity(e, query));
2626
- for (const match of matches) {
2627
- results.push({
2628
- householdId: household.id,
2629
- householdName: household.name,
2630
- entityType: type,
2631
- entity: redactEntity(match, type, privacyMode)
2632
- });
2633
- }
2634
- }
2635
- }
5107
+ const results = await executeSearch(
5108
+ args,
5109
+ privateKey,
5110
+ privacyMode
5111
+ );
2636
5112
  return {
2637
- content: [
2638
- {
2639
- type: "text",
2640
- text: JSON.stringify(results, null, 2)
2641
- }
2642
- ]
5113
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
2643
5114
  };
2644
5115
  }
2645
5116
  case "get_household_summary": {
2646
5117
  const { householdId } = args;
2647
- const households = await getHouseholds();
2648
- const household = households.find((h) => h.id === householdId);
2649
- if (!household) {
5118
+ const summary = await getHouseholdSummary(householdId, privateKey, getDecryptedEntities);
5119
+ if (!summary) {
2650
5120
  throw new Error(`Household not found: ${householdId}`);
2651
5121
  }
2652
- const entityTypes = [
2653
- "pet",
2654
- "property",
2655
- "vehicle",
2656
- "contact",
2657
- "insurance",
2658
- "bank_account",
2659
- "investment",
2660
- "subscription",
2661
- "maintenance_task",
2662
- "password",
2663
- "access_code"
2664
- ];
2665
- const counts = {};
2666
- for (const type of entityTypes) {
2667
- const entities = await getDecryptedEntities(householdId, type, privateKey);
2668
- counts[type] = entities.length;
2669
- }
2670
- const summary = {
2671
- household: {
2672
- id: household.id,
2673
- name: household.name
2674
- },
2675
- counts,
2676
- totalEntities: Object.values(counts).reduce((a, b) => a + b, 0)
2677
- };
2678
5122
  return {
2679
- content: [
2680
- {
2681
- type: "text",
2682
- text: JSON.stringify(summary, null, 2)
2683
- }
2684
- ]
5123
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
2685
5124
  };
2686
5125
  }
2687
5126
  case "get_expiring_items": {
2688
5127
  const { days = 30, householdId } = args;
2689
- const households = await getHouseholds();
2690
- const searchHouseholds = householdId ? households.filter((h) => h.id === householdId) : households;
2691
- const now = /* @__PURE__ */ new Date();
2692
- const cutoff = new Date(now.getTime() + days * 24 * 60 * 60 * 1e3);
2693
- const expiring = [];
2694
- for (const household of searchHouseholds) {
2695
- const insurance = await getDecryptedEntities(household.id, "insurance", privateKey);
2696
- for (const policy of insurance) {
2697
- if (policy.expirationDate) {
2698
- const expires = new Date(policy.expirationDate);
2699
- if (expires <= cutoff) {
2700
- expiring.push({
2701
- householdId: household.id,
2702
- householdName: household.name,
2703
- type: "insurance",
2704
- name: policy.name || policy.policyNumber,
2705
- expiresAt: policy.expirationDate,
2706
- daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
2707
- });
2708
- }
2709
- }
2710
- }
2711
- const vehicles = await getDecryptedEntities(household.id, "vehicle", privateKey);
2712
- for (const vehicle of vehicles) {
2713
- if (vehicle.registrationExpiration) {
2714
- const expires = new Date(vehicle.registrationExpiration);
2715
- if (expires <= cutoff) {
2716
- expiring.push({
2717
- householdId: household.id,
2718
- householdName: household.name,
2719
- type: "vehicle_registration",
2720
- name: `${vehicle.year || ""} ${vehicle.make || ""} ${vehicle.model || ""}`.trim(),
2721
- expiresAt: vehicle.registrationExpiration,
2722
- daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
2723
- });
2724
- }
2725
- }
2726
- }
2727
- const subscriptions = await getDecryptedEntities(household.id, "subscription", privateKey);
2728
- for (const sub of subscriptions) {
2729
- if (sub.renewalDate) {
2730
- const renews = new Date(sub.renewalDate);
2731
- if (renews <= cutoff) {
2732
- expiring.push({
2733
- householdId: household.id,
2734
- householdName: household.name,
2735
- type: "subscription",
2736
- name: sub.name || sub.serviceName,
2737
- expiresAt: sub.renewalDate,
2738
- daysUntil: Math.ceil((renews.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
2739
- });
2740
- }
2741
- }
2742
- }
2743
- }
2744
- expiring.sort((a, b) => a.daysUntil - b.daysUntil);
5128
+ const expiring = await getExpiringItems(days, householdId, privateKey);
2745
5129
  return {
2746
- content: [
2747
- {
2748
- type: "text",
2749
- text: JSON.stringify(expiring, null, 2)
2750
- }
2751
- ]
5130
+ content: [{ type: "text", text: JSON.stringify(expiring, null, 2) }]
5131
+ };
5132
+ }
5133
+ case "get_file": {
5134
+ const result = await executeFileDownload(client, args);
5135
+ return {
5136
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2752
5137
  };
2753
5138
  }
2754
5139
  case "refresh": {
2755
5140
  const synced2 = await syncIfNeeded(client, privateKey, true);
2756
5141
  return {
2757
- content: [
2758
- {
2759
- type: "text",
2760
- text: synced2 ? "Cache refreshed with latest data" : "Cache was already up to date"
2761
- }
2762
- ]
5142
+ content: [{
5143
+ type: "text",
5144
+ text: synced2 ? "Cache refreshed with latest data" : "Cache was already up to date"
5145
+ }]
2763
5146
  };
2764
5147
  }
2765
5148
  default:
2766
5149
  throw new Error(`Unknown tool: ${name}`);
2767
5150
  }
2768
5151
  });
2769
- server.setRequestHandler(import_types5.ListPromptsRequestSchema, async () => {
5152
+ server.setRequestHandler(import_types42.ListPromptsRequestSchema, async () => {
2770
5153
  return {
2771
5154
  prompts: [
2772
5155
  {
@@ -2799,12 +5182,12 @@ async function startServer(mode) {
2799
5182
  ]
2800
5183
  };
2801
5184
  });
2802
- server.setRequestHandler(import_types5.GetPromptRequestSchema, async (request) => {
5185
+ server.setRequestHandler(import_types42.GetPromptRequestSchema, async (request) => {
2803
5186
  const { name, arguments: args } = request.params;
2804
5187
  switch (name) {
2805
5188
  case "household_summary": {
2806
5189
  const householdId = args?.householdId;
2807
- const households = await getHouseholds();
5190
+ const households = await listHouseholds();
2808
5191
  const household = householdId ? households.find((h) => h.id === householdId) : households[0];
2809
5192
  if (!household) {
2810
5193
  throw new Error("No household found");
@@ -2829,7 +5212,7 @@ async function startServer(mode) {
2829
5212
  role: "user",
2830
5213
  content: {
2831
5214
  type: "text",
2832
- text: `What items are expiring in the next ${days} days? Include insurance policies, vehicle registrations, subscriptions, and any other items with expiration dates.`
5215
+ text: `What items are expiring in the next ${days} days? Include insurance policies, vehicle registrations, credentials (passports, licenses), subscriptions, and any other items with expiration dates.`
2833
5216
  }
2834
5217
  }
2835
5218
  ]
@@ -2855,18 +5238,6 @@ async function startServer(mode) {
2855
5238
  const transport = new import_stdio.StdioServerTransport();
2856
5239
  await server.connect(transport);
2857
5240
  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
5241
  }
2871
5242
  function parseResourceUri(uri) {
2872
5243
  const match = uri.match(/^estatehelm:\/\/([^/]+)(?:\/([^/]+))?(?:\/([^/]+))?(?:\/([^/]+))?$/);
@@ -2877,27 +5248,6 @@ function parseResourceUri(uri) {
2877
5248
  function formatEntityType(type) {
2878
5249
  return type.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2879
5250
  }
2880
- function searchEntity(entity, query) {
2881
- const lowerQuery = query.toLowerCase();
2882
- const searchFields = [
2883
- "name",
2884
- "title",
2885
- "description",
2886
- "notes",
2887
- "make",
2888
- "model",
2889
- "policyNumber",
2890
- "serviceName",
2891
- "username",
2892
- "email"
2893
- ];
2894
- for (const field of searchFields) {
2895
- if (entity[field] && String(entity[field]).toLowerCase().includes(lowerQuery)) {
2896
- return true;
2897
- }
2898
- }
2899
- return false;
2900
- }
2901
5251
 
2902
5252
  // src/index.ts
2903
5253
  var program = new import_commander.Command();