@t2000/engine 0.48.0 → 0.49.0

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.d.ts CHANGED
@@ -1954,7 +1954,9 @@ declare const renderCanvasTool: Tool<{
1954
1954
  } | undefined;
1955
1955
  }, unknown>;
1956
1956
 
1957
- declare const balanceCheckTool: Tool<{}, {
1957
+ declare const balanceCheckTool: Tool<{
1958
+ address?: string | undefined;
1959
+ }, {
1958
1960
  available: number;
1959
1961
  savings: number;
1960
1962
  debt: number;
@@ -1964,17 +1966,42 @@ declare const balanceCheckTool: Tool<{}, {
1964
1966
  stables: number;
1965
1967
  holdings: any[];
1966
1968
  saveableUsdc: number;
1969
+ address: string;
1970
+ isSelfQuery: boolean;
1967
1971
  }>;
1968
1972
 
1969
- declare const savingsInfoTool: Tool<{}, SavingsResult>;
1973
+ declare const savingsInfoTool: Tool<{
1974
+ address?: string | undefined;
1975
+ }, {
1976
+ address: string;
1977
+ isSelfQuery: boolean;
1978
+ positions: PositionEntry[];
1979
+ earnings: {
1980
+ totalYieldEarned: number;
1981
+ currentApy: number;
1982
+ dailyEarning: number;
1983
+ supplied: number;
1984
+ };
1985
+ fundStatus: {
1986
+ supplied: number;
1987
+ apy: number;
1988
+ earnedToday: number;
1989
+ earnedAllTime: number;
1990
+ projectedMonthly: number;
1991
+ };
1992
+ }>;
1970
1993
 
1971
- declare const healthCheckTool: Tool<{}, {
1994
+ declare const healthCheckTool: Tool<{
1995
+ address?: string | undefined;
1996
+ }, {
1972
1997
  healthFactor: number | null;
1973
1998
  supplied: number;
1974
1999
  borrowed: number;
1975
2000
  maxBorrow: number;
1976
2001
  liquidationThreshold: number;
1977
2002
  status: string;
2003
+ address: string;
2004
+ isSelfQuery: boolean;
1978
2005
  }>;
1979
2006
 
1980
2007
  type RateMap = Record<string, {
@@ -2274,8 +2301,11 @@ interface ActivitySummary {
2274
2301
  totalMovedUsd: number;
2275
2302
  netSavingsUsd: number;
2276
2303
  yieldEarnedUsd: number;
2304
+ address?: string;
2305
+ isSelfQuery?: boolean;
2277
2306
  }
2278
2307
  declare const activitySummaryTool: Tool<{
2308
+ address?: string | undefined;
2279
2309
  period?: "month" | "year" | "all" | "week" | undefined;
2280
2310
  }, ActivitySummary>;
2281
2311
 
package/dist/index.js CHANGED
@@ -725,6 +725,7 @@ function clearPriceMapCache() {
725
725
 
726
726
  // src/tools/balance.ts
727
727
  var GAS_RESERVE_SUI2 = 0.05;
728
+ var SUI_ADDRESS_REGEX = /^0x[a-fA-F0-9]{1,64}$/;
728
729
  var VSUI_COIN_TYPE = "0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT";
729
730
  var SUI_COIN_TYPE = "0x2::sui::SUI";
730
731
  var VSUI_FALLBACK_RATE = 1.05;
@@ -776,20 +777,37 @@ async function loadPortfolio(address, blockvisionApiKey, fallbackRpcUrl, cache)
776
777
  }
777
778
  var balanceCheckTool = buildTool({
778
779
  name: "balance_check",
779
- description: "Get the user's full balance breakdown. Returns wallet holdings (tokens the user owns \u2014 NOT savings), NAVI savings deposits (USDC deposited into NAVI Protocol earning yield), outstanding debt, pending rewards, gas reserve, total net worth, and saveableUsdc (only USDC can be deposited into savings). IMPORTANT: wallet holdings like GOLD, SUI, USDT are NOT savings positions \u2014 they are just tokens sitting in the wallet.",
780
- inputSchema: z.object({}),
781
- jsonSchema: { type: "object", properties: {}, required: [] },
780
+ description: "Get the full balance breakdown for the signed-in user OR any public Sui address. Returns wallet holdings (tokens the address owns \u2014 NOT savings), NAVI savings deposits (USDC deposited into NAVI Protocol earning yield), outstanding debt, pending rewards, gas reserve, total net worth, and saveableUsdc (only USDC can be deposited into savings). IMPORTANT: wallet holdings like GOLD, SUI, USDT are NOT savings positions \u2014 they are just tokens sitting in the wallet. Pass `address` to inspect a contact / watched / public wallet; defaults to the signed-in user when omitted.",
781
+ inputSchema: z.object({
782
+ address: z.string().regex(SUI_ADDRESS_REGEX).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
783
+ }),
784
+ jsonSchema: {
785
+ type: "object",
786
+ properties: {
787
+ address: {
788
+ type: "string",
789
+ pattern: "^0x[a-fA-F0-9]{1,64}$",
790
+ description: "Sui address to inspect (defaults to the signed-in wallet)"
791
+ }
792
+ },
793
+ required: []
794
+ },
782
795
  isReadOnly: true,
783
796
  // [v1.4 BlockVision] Wallet contents change after every send / swap /
784
797
  // save / etc. and the price half of this result is sourced from
785
798
  // BlockVision's Indexer REST API. Microcompact must NEVER dedupe these
786
799
  // calls — each one reflects a different on-chain + market snapshot.
787
800
  cacheable: false,
788
- async call(_input, context) {
789
- if (hasNaviMcp(context)) {
790
- const address = getWalletAddress(context);
801
+ async call(input, context) {
802
+ const targetAddress = input.address ?? context.walletAddress;
803
+ const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
804
+ if (hasNaviMcpGlobal(context)) {
805
+ if (!targetAddress) {
806
+ throw new Error("No wallet address provided. Sign in or pass `address` to inspect a public wallet.");
807
+ }
808
+ const address = targetAddress;
791
809
  const mgr = getMcpManager(context);
792
- const hasPositionFetcher = !!(context.positionFetcher && context.walletAddress);
810
+ const hasPositionFetcher = !!context.positionFetcher;
793
811
  const [portfolio, positions, rewards, serverPositions] = await Promise.all([
794
812
  loadPortfolio(
795
813
  address,
@@ -818,7 +836,7 @@ var balanceCheckTool = buildTool({
818
836
  console.warn("[balance_check] NAVI GET_AVAILABLE_REWARDS failed:", err);
819
837
  return null;
820
838
  }),
821
- hasPositionFetcher ? context.positionFetcher(context.walletAddress).catch((err) => {
839
+ hasPositionFetcher ? context.positionFetcher(address).catch((err) => {
822
840
  console.warn("[balance_check] positionFetcher failed:", err);
823
841
  return null;
824
842
  }) : Promise.resolve(null)
@@ -878,14 +896,22 @@ var balanceCheckTool = buildTool({
878
896
  stables: stablesUsd,
879
897
  holdings: visibleHoldings,
880
898
  saveableUsdc,
881
- priceSource: portfolio.source
899
+ priceSource: portfolio.source,
900
+ address,
901
+ isSelfQuery
882
902
  };
883
903
  const holdingsList = visibleHoldings.map((h) => `${h.symbol}: ${h.balance < 1 ? h.balance.toFixed(6) : h.balance.toFixed(2)} ($${h.usdValue.toFixed(2)})`).join(", ");
904
+ const subjectPrefix = isSelfQuery ? "Balance" : `Balance for ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
884
905
  return {
885
906
  data: bal,
886
- displayText: `Balance: $${bal.total.toFixed(2)} total. Wallet holdings (NOT savings): ${holdingsList || "none"}. NAVI savings deposits: $${bal.savings.toFixed(2)}. Saveable USDC (only USDC can be saved): ${saveableUsdc.toFixed(2)} USDC.`
907
+ displayText: `${subjectPrefix}: $${bal.total.toFixed(2)} total. Wallet holdings (NOT savings): ${holdingsList || "none"}. NAVI savings deposits: $${bal.savings.toFixed(2)}. Saveable USDC (only USDC can be saved): ${saveableUsdc.toFixed(2)} USDC.`
887
908
  };
888
909
  }
910
+ if (input.address && context.walletAddress && input.address.toLowerCase() !== context.walletAddress.toLowerCase()) {
911
+ throw new Error(
912
+ `Cannot inspect ${input.address.slice(0, 8)}\u2026 without NAVI MCP enabled. Configure NAVI MCP to enable third-party address reads.`
913
+ );
914
+ }
889
915
  const agent = requireAgent(context);
890
916
  const balance = await agent.balance();
891
917
  const gasReserveUsd = typeof balance.gasReserve === "number" ? balance.gasReserve : balance.gasReserve.usdEquiv ?? 0;
@@ -904,7 +930,9 @@ var balanceCheckTool = buildTool({
904
930
  total: balance.total,
905
931
  stables: stablesTotal,
906
932
  holdings: holdingsArr,
907
- saveableUsdc: sdkSaveableUsdc
933
+ saveableUsdc: sdkSaveableUsdc,
934
+ address: targetAddress ?? "",
935
+ isSelfQuery: true
908
936
  },
909
937
  displayText: `Balance: $${balance.total.toFixed(2)} total. Wallet: $${balance.available.toFixed(2)} available. NAVI savings deposits: $${balance.savings.toFixed(2)}. Saveable USDC (only USDC can be saved): ${sdkSaveableUsdc.toFixed(2)} USDC.`
910
938
  };
@@ -998,6 +1026,7 @@ async function fetchProtocolStats(manager, opts) {
998
1026
 
999
1027
  // src/tools/savings.ts
1000
1028
  var DUST_THRESHOLD_USD = 0.01;
1029
+ var SUI_ADDRESS_REGEX2 = /^0x[a-fA-F0-9]{1,64}$/;
1001
1030
  function buildSavingsFromPositions(sp) {
1002
1031
  const positions = [
1003
1032
  ...sp.supplies.filter((s) => s.amountUsd >= DUST_THRESHOLD_USD).map((s) => ({
@@ -1039,18 +1068,19 @@ function buildSavingsFromPositions(sp) {
1039
1068
  }
1040
1069
  };
1041
1070
  }
1042
- function formatSavingsDisplay(result) {
1071
+ function formatSavingsDisplay(result, isSelfQuery = true, address) {
1043
1072
  const { positions, earnings, fundStatus } = result;
1044
1073
  const supplies = positions.filter((p) => p.type === "supply");
1045
1074
  const borrows = positions.filter((p) => p.type === "borrow");
1075
+ const subjectPrefix = isSelfQuery || !address ? "" : `${address.slice(0, 6)}\u2026${address.slice(-4)} \u2014 `;
1046
1076
  const lines = [];
1047
1077
  if (supplies.length > 0) {
1048
- lines.push(`Savings: $${fundStatus.supplied.toFixed(2)} at ${(earnings.currentApy * 100).toFixed(2)}% blended APY`);
1078
+ lines.push(`${subjectPrefix}Savings: $${fundStatus.supplied.toFixed(2)} at ${(earnings.currentApy * 100).toFixed(2)}% blended APY`);
1049
1079
  for (const s of supplies) {
1050
1080
  lines.push(` ${s.symbol}: ${s.amount.toFixed(s.amount < 1 ? 6 : 2)} ($${s.valueUsd.toFixed(2)}) at ${(s.apy * 100).toFixed(2)}% APY`);
1051
1081
  }
1052
1082
  } else {
1053
- lines.push("No savings positions.");
1083
+ lines.push(`${subjectPrefix}No savings positions.`);
1054
1084
  }
1055
1085
  if (borrows.length > 0) {
1056
1086
  const totalDebt = borrows.reduce((s, b) => s + b.valueUsd, 0);
@@ -1062,26 +1092,44 @@ function formatSavingsDisplay(result) {
1062
1092
  }
1063
1093
  var savingsInfoTool = buildTool({
1064
1094
  name: "savings_info",
1065
- description: "Get detailed savings positions and earnings: current deposits by protocol, APY, total yield earned, daily earning rate, and projected monthly returns.",
1066
- inputSchema: z.object({}),
1067
- jsonSchema: { type: "object", properties: {}, required: [] },
1095
+ description: "Get detailed savings positions and earnings for the signed-in user OR any public Sui address: current deposits by protocol, APY, total yield earned, daily earning rate, and projected monthly returns. Pass `address` to inspect a contact / watched / public wallet; defaults to the signed-in user when omitted.",
1096
+ inputSchema: z.object({
1097
+ address: z.string().regex(SUI_ADDRESS_REGEX2).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
1098
+ }),
1099
+ jsonSchema: {
1100
+ type: "object",
1101
+ properties: {
1102
+ address: {
1103
+ type: "string",
1104
+ pattern: "^0x[a-fA-F0-9]{1,64}$",
1105
+ description: "Sui address to inspect (defaults to the signed-in wallet)"
1106
+ }
1107
+ },
1108
+ required: []
1109
+ },
1068
1110
  isReadOnly: true,
1069
1111
  // [v1.5.1] NAVI deposits change on save_deposit / withdraw / claim.
1070
1112
  // Each call reflects a fresh on-chain snapshot — never dedupe.
1071
1113
  cacheable: false,
1072
- async call(_input, context) {
1073
- if (context.positionFetcher && context.walletAddress) {
1074
- const sp = await context.positionFetcher(context.walletAddress);
1114
+ async call(input, context) {
1115
+ const targetAddress = input.address ?? context.walletAddress;
1116
+ const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
1117
+ if (context.positionFetcher && targetAddress) {
1118
+ const sp = await context.positionFetcher(targetAddress);
1075
1119
  const result2 = buildSavingsFromPositions(sp);
1076
- return { data: result2, displayText: formatSavingsDisplay(result2) };
1120
+ const stamped2 = { ...result2, address: targetAddress, isSelfQuery };
1121
+ return { data: stamped2, displayText: formatSavingsDisplay(result2, isSelfQuery, targetAddress) };
1077
1122
  }
1078
- if (hasNaviMcp(context)) {
1079
- const savings = await fetchSavings(
1080
- getMcpManager(context),
1081
- getWalletAddress(context)
1082
- );
1123
+ if (hasNaviMcpGlobal(context) && targetAddress) {
1124
+ const savings = await fetchSavings(getMcpManager(context), targetAddress);
1083
1125
  savings.positions = savings.positions.filter((p) => p.valueUsd >= DUST_THRESHOLD_USD);
1084
- return { data: savings, displayText: formatSavingsDisplay(savings) };
1126
+ const stamped2 = { ...savings, address: targetAddress, isSelfQuery };
1127
+ return { data: stamped2, displayText: formatSavingsDisplay(savings, isSelfQuery, targetAddress) };
1128
+ }
1129
+ if (input.address && context.walletAddress && input.address.toLowerCase() !== context.walletAddress.toLowerCase()) {
1130
+ throw new Error(
1131
+ `Cannot inspect ${input.address.slice(0, 8)}\u2026 without NAVI MCP or a positionFetcher. Configure NAVI MCP to enable third-party address reads.`
1132
+ );
1085
1133
  }
1086
1134
  const agent = requireAgent(context);
1087
1135
  const [posResult, earnings, fundStatus] = await Promise.all([
@@ -1114,9 +1162,11 @@ var savingsInfoTool = buildTool({
1114
1162
  projectedMonthly: fundStatus.projectedMonthly
1115
1163
  }
1116
1164
  };
1117
- return { data: result, displayText: formatSavingsDisplay(result) };
1165
+ const stamped = { ...result, address: targetAddress ?? "", isSelfQuery: true };
1166
+ return { data: stamped, displayText: formatSavingsDisplay(result, true, void 0) };
1118
1167
  }
1119
1168
  });
1169
+ var SUI_ADDRESS_REGEX3 = /^0x[a-fA-F0-9]{1,64}$/;
1120
1170
  var DEBT_DUST_USD = 0.01;
1121
1171
  function hfStatus(hf, borrowed) {
1122
1172
  if (borrowed <= DEBT_DUST_USD) return "healthy";
@@ -1130,24 +1180,39 @@ function serializeHf(hf, borrowed) {
1130
1180
  if (hf == null || !Number.isFinite(hf)) return null;
1131
1181
  return hf;
1132
1182
  }
1133
- function displayHfText(hf, borrowed, status) {
1183
+ function displayHfText(hf, borrowed, status, isSelfQuery = true, address) {
1184
+ const subject = isSelfQuery || !address ? "Health Factor" : `Health Factor for ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
1134
1185
  if (hf == null) {
1135
- return `Health Factor: \u221E (${status} \u2014 no debt)`;
1186
+ return `${subject}: \u221E (${status} \u2014 no debt)`;
1136
1187
  }
1137
- return `Health Factor: ${hf.toFixed(2)} (${status})`;
1188
+ return `${subject}: ${hf.toFixed(2)} (${status})`;
1138
1189
  }
1139
1190
  var healthCheckTool = buildTool({
1140
1191
  name: "health_check",
1141
- description: 'Check the lending health factor: current HF ratio, total supplied collateral, total borrowed, max additional borrow capacity, and liquidation threshold. HF < 1.5 is risky, < 1.2 is critical. When the user has no debt the tool returns healthFactor=null (semantically infinity) \u2014 render that as "Healthy" / \u221E, never as 0 or "Critical".',
1142
- inputSchema: z.object({}),
1143
- jsonSchema: { type: "object", properties: {}, required: [] },
1192
+ description: 'Check the lending health factor for the signed-in user OR any public Sui address: current HF ratio, total supplied collateral, total borrowed, max additional borrow capacity, and liquidation threshold. HF < 1.5 is risky, < 1.2 is critical. When the address has no debt the tool returns healthFactor=null (semantically infinity) \u2014 render that as "Healthy" / \u221E, never as 0 or "Critical". Pass `address` to inspect a contact / watched / public wallet; defaults to the signed-in user when omitted.',
1193
+ inputSchema: z.object({
1194
+ address: z.string().regex(SUI_ADDRESS_REGEX3).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
1195
+ }),
1196
+ jsonSchema: {
1197
+ type: "object",
1198
+ properties: {
1199
+ address: {
1200
+ type: "string",
1201
+ pattern: "^0x[a-fA-F0-9]{1,64}$",
1202
+ description: "Sui address to inspect (defaults to the signed-in wallet)"
1203
+ }
1204
+ },
1205
+ required: []
1206
+ },
1144
1207
  isReadOnly: true,
1145
1208
  // [v1.5.1] Health factor changes on every borrow / repay / collateral
1146
1209
  // movement and even passively as oracle prices update. Never dedupe.
1147
1210
  cacheable: false,
1148
- async call(_input, context) {
1149
- if (context.positionFetcher && context.walletAddress) {
1150
- const sp = await context.positionFetcher(context.walletAddress);
1211
+ async call(input, context) {
1212
+ const targetAddress = input.address ?? context.walletAddress;
1213
+ const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
1214
+ if (context.positionFetcher && targetAddress) {
1215
+ const sp = await context.positionFetcher(targetAddress);
1151
1216
  const borrowed2 = sp.borrows;
1152
1217
  const rawHf = sp.healthFactor ?? (borrowed2 > 0 ? 0 : Infinity);
1153
1218
  const status2 = hfStatus(rawHf, borrowed2);
@@ -1159,24 +1224,28 @@ var healthCheckTool = buildTool({
1159
1224
  borrowed: borrowed2,
1160
1225
  maxBorrow: sp.maxBorrow,
1161
1226
  liquidationThreshold: 0,
1162
- status: status2
1227
+ status: status2,
1228
+ address: targetAddress,
1229
+ isSelfQuery
1163
1230
  },
1164
- displayText: displayHfText(transportHf2, borrowed2, status2)
1231
+ displayText: displayHfText(transportHf2, borrowed2, status2, isSelfQuery, targetAddress)
1165
1232
  };
1166
1233
  }
1167
- if (hasNaviMcp(context)) {
1168
- const hf2 = await fetchHealthFactor(
1169
- getMcpManager(context),
1170
- getWalletAddress(context)
1171
- );
1234
+ if (hasNaviMcpGlobal(context) && targetAddress) {
1235
+ const hf2 = await fetchHealthFactor(getMcpManager(context), targetAddress);
1172
1236
  const borrowed2 = hf2.borrowed;
1173
1237
  const status2 = hfStatus(hf2.healthFactor, borrowed2);
1174
1238
  const transportHf2 = serializeHf(hf2.healthFactor, borrowed2);
1175
1239
  return {
1176
- data: { ...hf2, healthFactor: transportHf2, status: status2 },
1177
- displayText: displayHfText(transportHf2, borrowed2, status2)
1240
+ data: { ...hf2, healthFactor: transportHf2, status: status2, address: targetAddress, isSelfQuery },
1241
+ displayText: displayHfText(transportHf2, borrowed2, status2, isSelfQuery, targetAddress)
1178
1242
  };
1179
1243
  }
1244
+ if (input.address && context.walletAddress && input.address.toLowerCase() !== context.walletAddress.toLowerCase()) {
1245
+ throw new Error(
1246
+ `Cannot inspect ${input.address.slice(0, 8)}\u2026 without NAVI MCP or a positionFetcher. Configure NAVI MCP to enable third-party address reads.`
1247
+ );
1248
+ }
1180
1249
  const agent = requireAgent(context);
1181
1250
  const hf = await agent.healthFactor();
1182
1251
  const borrowed = hf.borrowed;
@@ -1189,9 +1258,11 @@ var healthCheckTool = buildTool({
1189
1258
  borrowed,
1190
1259
  maxBorrow: hf.maxBorrow,
1191
1260
  liquidationThreshold: hf.liquidationThreshold,
1192
- status
1261
+ status,
1262
+ address: targetAddress ?? "",
1263
+ isSelfQuery: true
1193
1264
  },
1194
- displayText: displayHfText(transportHf, borrowed, status)
1265
+ displayText: displayHfText(transportHf, borrowed, status, true, void 0)
1195
1266
  };
1196
1267
  }
1197
1268
  });
@@ -1400,14 +1471,14 @@ async function queryHistoryByDate(rpcUrl, address, targetDate, limit) {
1400
1471
  }
1401
1472
  var HISTORY_ACTIONS = ["send", "lending", "swap", "transaction"];
1402
1473
  var DEFAULT_LOOKBACK_DAYS = 30;
1403
- var SUI_ADDRESS_REGEX = /^0x[0-9a-fA-F]{64}$/;
1474
+ var SUI_ADDRESS_REGEX4 = /^0x[0-9a-fA-F]{64}$/;
1404
1475
  var transactionHistoryTool = buildTool({
1405
1476
  name: "transaction_history",
1406
1477
  description: 'Retrieve recent transaction history (last 30 days by default): sends, saves, withdrawals, borrows, repayments, swaps, and rewards claims. Renders a rich transaction card.\n\nBy default, queries the SIGNED-IN USER\'S history. To inspect another wallet (a saved contact, a watched address, any public Sui address), pass `address` \u2014 e.g. user asks "show funkii\'s recent transactions" with funkii at 0x40cd\u20263e62, call with `address: "0x40cd\u20263e62"`. To filter the user\'s own history to a specific counterparty (user asks "show transactions WITH funkii"), pass `counterparty` \u2014 keeps the query rooted in the user\'s wallet but shows only rows where funkii is the recipient or sender.\n\nFilter args: `date` (YYYY-MM-DD), `action` (send/lending/swap), `minUsd` (minimum amount in USD \u2014 use this for "transactions over $X" instead of post-filtering), `assetSymbol` (e.g. "USDC", "SUI"), `direction` ("in" or "out"). The card itself respects all filters \u2014 never re-list the rows in narration.\n\nInternally queries both `FromAddress` and `ToAddress` filters in parallel and dedupes by digest, so pure-receive transactions (someone sends to the queried address with no balance-affecting outbound) are no longer dropped.',
1407
1478
  inputSchema: z.object({
1408
1479
  limit: z.number().int().min(1).max(50).optional(),
1409
- address: z.string().regex(SUI_ADDRESS_REGEX, "Must be a 0x-prefixed 64-hex Sui address").optional().describe("Sui address to query history FOR. When omitted, defaults to the signed-in user's wallet. Pass this when the user asks about a contact's, watched address's, or any other public wallet's history."),
1410
- counterparty: z.string().regex(SUI_ADDRESS_REGEX, "Must be a 0x-prefixed 64-hex Sui address").optional().describe('Sui address to filter rows by \u2014 only transactions where the queried address sent to or received from this counterparty are returned. Use for "show transactions with <contact>" queries. Compares against `tx.recipient` (case-insensitive).'),
1480
+ address: z.string().regex(SUI_ADDRESS_REGEX4, "Must be a 0x-prefixed 64-hex Sui address").optional().describe("Sui address to query history FOR. When omitted, defaults to the signed-in user's wallet. Pass this when the user asks about a contact's, watched address's, or any other public wallet's history."),
1481
+ counterparty: z.string().regex(SUI_ADDRESS_REGEX4, "Must be a 0x-prefixed 64-hex Sui address").optional().describe('Sui address to filter rows by \u2014 only transactions where the queried address sent to or received from this counterparty are returned. Use for "show transactions with <contact>" queries. Compares against `tx.recipient` (case-insensitive).'),
1411
1482
  date: z.string().optional().describe("Specific date to search for transactions (YYYY-MM-DD format). Paginates back to find that day."),
1412
1483
  action: z.enum(HISTORY_ACTIONS).optional().describe("Filter by action: send, lending, swap, or transaction."),
1413
1484
  minUsd: z.number().min(0).optional().describe('Minimum transaction amount in USD. Use this for "transactions over $X" \u2014 the amount is converted to USD using the asset price snapshot.'),
@@ -3028,14 +3099,14 @@ Use when the user asks for a visual chart, simulator, or financial overview. Pic
3028
3099
 
3029
3100
  - activity_heatmap \u2014 on-chain transaction history as a GitHub-style heatmap (WORKS NOW \u2014 accepts \`params.address\` to inspect any public Sui wallet; defaults to the signed-in user)
3030
3101
  - portfolio_timeline \u2014 net worth over time, wallet/savings/debt breakdown (WORKS NOW \u2014 accepts \`params.address\` for any public wallet; defaults to the signed-in user)
3031
- - yield_projector \u2014 compound yield simulator with amount/APY/period sliders (WORKS NOW \u2014 client-side)
3032
- - health_simulator \u2014 borrow health factor simulator with collateral/debt sliders (WORKS NOW \u2014 uses current position)
3033
- - dca_planner \u2014 savings plan curve for regular monthly deposits (WORKS NOW \u2014 client-side)
3102
+ - yield_projector \u2014 compound yield simulator with amount/APY/period sliders (WORKS NOW \u2014 client-side, no address needed)
3103
+ - health_simulator \u2014 borrow health factor simulator with collateral/debt sliders (WORKS NOW \u2014 accepts \`params.address\` for any public wallet; defaults to the signed-in user's current position)
3104
+ - dca_planner \u2014 savings plan curve for regular monthly deposits (WORKS NOW \u2014 client-side, no address needed)
3034
3105
  - spending_breakdown \u2014 spending by service category (WORKS NOW \u2014 accepts \`params.address\` for any public wallet; defaults to the signed-in user)
3035
3106
  - watch_address \u2014 portfolio overview for any public Sui address (WORKS NOW \u2014 pass \`params.address\`)
3036
- - full_portfolio \u2014 4-panel overview: savings, health, activity, spending (WORKS NOW \u2014 aggregates all data)
3107
+ - full_portfolio \u2014 4-panel overview: savings, health, activity, spending (WORKS NOW \u2014 accepts \`params.address\` for any public wallet; defaults to the signed-in user)
3037
3108
 
3038
- When the user asks to inspect a saved contact or watched address \u2014 e.g. "show funkii's activity heatmap", "what's funkii's portfolio look like", "spending breakdown for 0x40cd\u2026" \u2014 pass that wallet's address as \`params.address\`. The four address-aware templates (activity_heatmap, portfolio_timeline, spending_breakdown, watch_address) will scope their data fetch to that address; the rest target the signed-in user regardless of params.
3109
+ When the user asks to inspect a saved contact or watched address \u2014 e.g. "show funkii's activity heatmap", "what's funkii's portfolio look like", "spending breakdown for 0x40cd\u2026", "give me a full portfolio overview of 0x40cd\u2026" \u2014 pass that wallet's address as \`params.address\`. Six of the eight templates (activity_heatmap, portfolio_timeline, spending_breakdown, watch_address, health_simulator, full_portfolio) will scope their data fetch to that address; only the pure client-side simulators (yield_projector, dca_planner) ignore params.address.
3039
3110
 
3040
3111
  Always prefer the canvas for visualisation requests. After rendering, offer to explain what the user sees.`,
3041
3112
  inputSchema: z.object({
@@ -3043,7 +3114,7 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3043
3114
  params: z.object({
3044
3115
  period: z.enum(["1m", "3m", "6m", "1y"]).optional().describe("Time period for time-based templates"),
3045
3116
  address: z.string().optional().describe(
3046
- "Sui address for the four address-aware templates (activity_heatmap, portfolio_timeline, spending_breakdown, watch_address). Defaults to the signed-in user; pass an explicit address to inspect a contact, watched wallet, or any other public address."
3117
+ "Sui address for the six address-aware templates (activity_heatmap, portfolio_timeline, spending_breakdown, watch_address, health_simulator, full_portfolio). Defaults to the signed-in user; pass an explicit address to inspect a contact, watched wallet, or any other public address."
3047
3118
  )
3048
3119
  }).optional()
3049
3120
  }),
@@ -3077,7 +3148,20 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3077
3148
  return { address: target, isSelfRender };
3078
3149
  };
3079
3150
  if (template === "full_portfolio") {
3080
- const pos = context.serverPositions;
3151
+ const { address, isSelfRender } = resolveAddressTarget();
3152
+ if (!address) {
3153
+ return {
3154
+ data: {
3155
+ __canvas: true,
3156
+ template,
3157
+ title,
3158
+ templateData: { available: false, message: "Full Portfolio needs an address." }
3159
+ },
3160
+ displayText: "Full Portfolio requires an address."
3161
+ };
3162
+ }
3163
+ const titleSuffix = isSelfRender ? "" : ` \u2014 ${address.slice(0, 6)}\u2026${address.slice(-4)}`;
3164
+ const pos = isSelfRender ? context.serverPositions : null;
3081
3165
  const rate = normalizeSavingsRate(pos?.savingsRate);
3082
3166
  const savings = pos?.savings ?? 0;
3083
3167
  const borrows = pos?.borrows ?? 0;
@@ -3085,17 +3169,18 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3085
3169
  data: {
3086
3170
  __canvas: true,
3087
3171
  template,
3088
- title,
3172
+ title: `${title}${titleSuffix}`,
3089
3173
  templateData: {
3090
3174
  available: true,
3091
- address: context.walletAddress ?? "",
3175
+ address,
3176
+ isSelfRender,
3092
3177
  currentSavings: savings,
3093
3178
  currentDebt: borrows,
3094
3179
  healthFactor: pos?.healthFactor ?? null,
3095
3180
  savingsRate: rate
3096
3181
  }
3097
3182
  },
3098
- displayText: `Opened Full Portfolio Overview.`
3183
+ displayText: isSelfRender ? `Opened Full Portfolio Overview.` : `Opened Full Portfolio Overview for ${address.slice(0, 6)}\u2026${address.slice(-4)}.`
3099
3184
  };
3100
3185
  }
3101
3186
  if (template === "watch_address") {
@@ -3226,20 +3311,28 @@ Always prefer the canvas for visualisation requests. After rendering, offer to e
3226
3311
  };
3227
3312
  }
3228
3313
  if (template === "health_simulator") {
3229
- const roundedDebt = totalBorrows >= 1 ? Math.round(totalBorrows) : totalBorrows > 0 ? parseFloat(totalBorrows.toFixed(4)) : 0;
3314
+ const { address: targetAddress, isSelfRender } = resolveAddressTarget();
3315
+ const seedFromPos = isSelfRender;
3316
+ const seedSavings = seedFromPos ? totalSavings : 0;
3317
+ const seedBorrows = seedFromPos ? totalBorrows : 0;
3318
+ const seedHf = seedFromPos ? healthFactor : null;
3319
+ const roundedDebt = seedBorrows >= 1 ? Math.round(seedBorrows) : seedBorrows > 0 ? parseFloat(seedBorrows.toFixed(4)) : 0;
3320
+ const titleSuffix = !targetAddress || isSelfRender ? "" : ` \u2014 ${targetAddress.slice(0, 6)}\u2026${targetAddress.slice(-4)}`;
3230
3321
  return {
3231
3322
  data: {
3232
3323
  __canvas: true,
3233
3324
  template,
3234
- title,
3325
+ title: `${title}${titleSuffix}`,
3235
3326
  templateData: {
3236
3327
  available: true,
3237
- initialCollateral: totalSavings > 0 ? Math.round(totalSavings) : 1500,
3238
- initialDebt: roundedDebt > 0 ? roundedDebt : totalSavings > 0 ? 0 : 500,
3239
- currentHf: healthFactor
3328
+ address: targetAddress ?? "",
3329
+ isSelfRender,
3330
+ initialCollateral: seedSavings > 0 ? Math.round(seedSavings) : 1500,
3331
+ initialDebt: roundedDebt > 0 ? roundedDebt : seedSavings > 0 ? 0 : 500,
3332
+ currentHf: seedHf
3240
3333
  }
3241
3334
  },
3242
- displayText: `Opened Health Factor Simulator. Current HF: ${healthFactor !== null ? healthFactor.toFixed(2) : "no active position"}.`
3335
+ displayText: isSelfRender ? `Opened Health Factor Simulator. Current HF: ${healthFactor !== null ? healthFactor.toFixed(2) : "no active position"}.` : `Opened Health Factor Simulator${titleSuffix}. The simulator will fetch the current health factor for that wallet.`
3243
3336
  };
3244
3337
  }
3245
3338
  if (template === "dca_planner") {
@@ -3354,49 +3447,62 @@ var yieldSummaryTool = buildTool({
3354
3447
  }
3355
3448
  }
3356
3449
  });
3450
+ var SUI_ADDRESS_REGEX5 = /^0x[a-fA-F0-9]{1,64}$/;
3357
3451
  var activitySummaryTool = buildTool({
3358
3452
  name: "activity_summary",
3359
- description: "Returns a categorised DeFi activity summary for a period: transaction count, breakdown by action type (saves, sends, borrows, repayments, swaps, payments), total moved, net savings change, and yield earned. Use when the user asks about their activity, transaction history summary, or what they have done recently.",
3453
+ description: "Returns a categorised DeFi activity summary for the signed-in user OR any public Sui address: transaction count, breakdown by action type (saves, sends, borrows, repayments, swaps, payments), total moved, net savings change, and yield earned. Use when the user asks about activity, transaction history summary, or what someone has done recently. Pass `address` to inspect a contact / watched / public wallet; defaults to the signed-in user when omitted.",
3360
3454
  inputSchema: z.object({
3361
- period: z.enum(["week", "month", "year", "all"]).optional().describe("Time period. Defaults to current month.")
3455
+ period: z.enum(["week", "month", "year", "all"]).optional().describe("Time period. Defaults to current month."),
3456
+ address: z.string().regex(SUI_ADDRESS_REGEX5).optional().describe("Sui address to inspect (defaults to the signed-in wallet)")
3362
3457
  }),
3363
3458
  jsonSchema: {
3364
3459
  type: "object",
3365
3460
  properties: {
3366
- period: { type: "string", enum: ["week", "month", "year", "all"] }
3461
+ period: { type: "string", enum: ["week", "month", "year", "all"] },
3462
+ address: {
3463
+ type: "string",
3464
+ pattern: "^0x[a-fA-F0-9]{1,64}$",
3465
+ description: "Sui address to inspect (defaults to the signed-in wallet)"
3466
+ }
3367
3467
  }
3368
3468
  },
3369
3469
  isReadOnly: true,
3370
3470
  async call(input, context) {
3371
3471
  const period = input.period ?? "month";
3372
3472
  const apiUrl = context.env?.AUDRIC_INTERNAL_API_URL;
3373
- const address = context.walletAddress;
3473
+ const targetAddress = input.address ?? context.walletAddress;
3474
+ const isSelfQuery = !!context.walletAddress && !!targetAddress && targetAddress.toLowerCase() === context.walletAddress.toLowerCase();
3374
3475
  const empty = {
3375
3476
  period,
3376
3477
  totalTransactions: 0,
3377
3478
  byAction: [],
3378
3479
  totalMovedUsd: 0,
3379
3480
  netSavingsUsd: 0,
3380
- yieldEarnedUsd: 0
3481
+ yieldEarnedUsd: 0,
3482
+ address: targetAddress,
3483
+ isSelfQuery
3381
3484
  };
3382
- if (!apiUrl || !address) {
3485
+ if (!apiUrl || !targetAddress) {
3383
3486
  return { data: empty, displayText: "Activity summary not available." };
3384
3487
  }
3385
3488
  try {
3489
+ const callerHeader = context.walletAddress ?? targetAddress;
3386
3490
  const res = await fetch(
3387
- `${apiUrl}/api/analytics/activity-summary?address=${address}&period=${period}`,
3388
- { headers: { "x-sui-address": address }, signal: context.signal }
3491
+ `${apiUrl}/api/analytics/activity-summary?address=${targetAddress}&period=${period}`,
3492
+ { headers: { "x-sui-address": callerHeader }, signal: context.signal }
3389
3493
  );
3390
3494
  if (!res.ok) {
3391
3495
  return { data: empty, displayText: `Could not fetch activity data (HTTP ${res.status}).` };
3392
3496
  }
3393
- const data = await res.json();
3497
+ const raw = await res.json();
3498
+ const data = { ...raw, address: targetAddress, isSelfQuery };
3394
3499
  const sorted = [...data.byAction ?? []].sort((a, b) => b.count - a.count);
3395
3500
  const top = sorted.slice(0, 3).map((a) => `${a.action} (${a.count})`).join(", ");
3396
3501
  const periodLabel = data.period === "all" ? "all time" : `this ${data.period}`;
3502
+ const subjectPrefix = isSelfQuery ? "" : `${targetAddress.slice(0, 6)}\u2026${targetAddress.slice(-4)} \u2014 `;
3397
3503
  return {
3398
3504
  data,
3399
- displayText: data.totalTransactions > 0 ? `${data.totalTransactions} transactions ${periodLabel}. Top: ${top}. Total moved: $${data.totalMovedUsd.toFixed(2)}. Net savings: $${data.netSavingsUsd.toFixed(2)}.` : `No activity recorded for ${periodLabel}.`
3505
+ displayText: data.totalTransactions > 0 ? `${subjectPrefix}${data.totalTransactions} transactions ${periodLabel}. Top: ${top}. Total moved: $${data.totalMovedUsd.toFixed(2)}. Net savings: $${data.netSavingsUsd.toFixed(2)}.` : `${subjectPrefix}No activity recorded for ${periodLabel}.`
3400
3506
  };
3401
3507
  } catch {
3402
3508
  return { data: empty, displayText: "Error fetching activity summary." };
@@ -3873,7 +3979,7 @@ function guardCostWarning(tool, _call, conversationText) {
3873
3979
  message: "This action has a monetary cost. Confirm the user is aware before proceeding."
3874
3980
  };
3875
3981
  }
3876
- var SUI_ADDRESS_REGEX2 = /^0x[a-fA-F0-9]{64}$/;
3982
+ var SUI_ADDRESS_REGEX6 = /^0x[a-fA-F0-9]{64}$/;
3877
3983
  function normalizeAddress(addr) {
3878
3984
  return addr.trim().toLowerCase();
3879
3985
  }
@@ -3938,7 +4044,7 @@ function guardAddressSource(tool, call, userText, contacts, walletAddress) {
3938
4044
  if (!rawTo) {
3939
4045
  return { verdict: "pass", gate: "address_source", tier: "safety" };
3940
4046
  }
3941
- if (!SUI_ADDRESS_REGEX2.test(rawTo)) {
4047
+ if (!SUI_ADDRESS_REGEX6.test(rawTo)) {
3942
4048
  return { verdict: "pass", gate: "address_source", tier: "safety" };
3943
4049
  }
3944
4050
  const normalizedTo = normalizeAddress(rawTo);