shll-skills 2.0.11 → 2.1.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/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: shll-run
3
3
  description: Execute DeFi transactions on BSC via SHLL AgentNFA. The AI handles all commands — users only need to chat.
4
- version: 4.0.0
4
+ version: 4.1.0
5
5
  author: SHLL Team
6
6
  website: https://shll.run
7
7
  twitter: https://twitter.com/shllrun
@@ -175,11 +175,13 @@ Examples:
175
175
  | `shll-run search --query <TEXT>` | Search token by name |
176
176
  | `shll-run tokens` | List known tokens |
177
177
 
178
- ### Risk Management
178
+ ### Risk Management & Audit
179
179
  | Command | What it does |
180
180
  |---------|-------------|
181
- | `shll-run policies -k <ID>` | View active policies |
181
+ | `shll-run policies -k <ID>` | View active policies + human-readable summary |
182
182
  | `shll-run config -k <ID> --tx-limit <BNB> --daily-limit <BNB> --cooldown <SEC>` | Tighten risk limits |
183
+ | `shll-run status -k <ID>` | One-shot security overview (vault, operator, policies, activity) |
184
+ | `shll-run history -k <ID> [--limit N]` | Recent transactions + policy rejections |
183
185
 
184
186
  **Supported tokens:** BNB, USDC, USDT, WBNB, CAKE, ETH, BTCB, DAI, BUSD, or any 0x address.
185
187
 
package/dist/index.js CHANGED
@@ -1346,6 +1346,7 @@ policiesCmd.action(async (opts) => {
1346
1346
  const publicClient = (0, import_viem2.createPublicClient)({ chain: import_chains2.bsc, transport: (0, import_viem2.http)(rpcUrl) });
1347
1347
  const policies = await client.getPolicies(tokenId);
1348
1348
  const enriched = [];
1349
+ const summaryParts = [];
1349
1350
  for (const p of policies) {
1350
1351
  const entry = {
1351
1352
  name: p.policyTypeName,
@@ -1361,13 +1362,16 @@ policiesCmd.action(async (opts) => {
1361
1362
  args: [tokenId]
1362
1363
  });
1363
1364
  const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
1365
+ const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
1366
+ const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
1364
1367
  entry.currentConfig = {
1365
1368
  maxPerTx: maxPerTx.toString(),
1366
- maxPerTxBnb: (Number(maxPerTx) / 1e18).toFixed(4),
1369
+ maxPerTxBnb: txBnb,
1367
1370
  maxPerDay: maxPerDay.toString(),
1368
- maxPerDayBnb: (Number(maxPerDay) / 1e18).toFixed(4),
1371
+ maxPerDayBnb: dayBnb,
1369
1372
  maxSlippageBps: maxSlippageBps.toString()
1370
1373
  };
1374
+ summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
1371
1375
  } catch {
1372
1376
  }
1373
1377
  }
@@ -1379,13 +1383,34 @@ policiesCmd.action(async (opts) => {
1379
1383
  functionName: "cooldownSeconds",
1380
1384
  args: [tokenId]
1381
1385
  });
1382
- entry.currentConfig = { cooldownSeconds: cd.toString() };
1386
+ const secs = Number(cd);
1387
+ entry.currentConfig = { cooldownSeconds: secs.toString() };
1388
+ summaryParts.push(`Cooldown ${secs}s between transactions`);
1383
1389
  } catch {
1384
1390
  }
1385
1391
  }
1392
+ if (p.policyTypeName === "receiver_guard") {
1393
+ summaryParts.push("Outbound transfers restricted (ReceiverGuard)");
1394
+ }
1395
+ if (p.policyTypeName === "dex_whitelist") {
1396
+ summaryParts.push("Only whitelisted DEXs allowed");
1397
+ }
1398
+ if (p.policyTypeName === "token_whitelist") {
1399
+ summaryParts.push("Only whitelisted tokens allowed");
1400
+ }
1401
+ if (p.policyTypeName === "defi_guard") {
1402
+ summaryParts.push("DeFi interactions validated by DeFiGuard");
1403
+ }
1386
1404
  enriched.push(entry);
1387
1405
  }
1388
- output({ status: "success", tokenId: tokenId.toString(), policies: enriched });
1406
+ const humanSummary = summaryParts.length > 0 ? summaryParts.join(" | ") : "No configurable policies found";
1407
+ output({
1408
+ status: "success",
1409
+ tokenId: tokenId.toString(),
1410
+ humanSummary,
1411
+ securityNote: "Operator wallet CANNOT withdraw vault funds or transfer Agent NFT \u2014 only owner can.",
1412
+ policies: enriched
1413
+ });
1389
1414
  } catch (error) {
1390
1415
  const message = error instanceof Error ? error.message : "Unknown error";
1391
1416
  output({ status: "error", message });
@@ -1467,7 +1492,7 @@ configCmd.action(async (opts) => {
1467
1492
  process.exit(1);
1468
1493
  }
1469
1494
  });
1470
- var DEFAULT_INDEXER = "https://indexer.shll.run";
1495
+ var DEFAULT_INDEXER = "https://indexer-mainnet.shll.run";
1471
1496
  var listingsCmd = new import_commander.Command("listings").description("List all available agent templates for rent").option("--indexer <url>", "Indexer API URL", DEFAULT_INDEXER).action(async (opts) => {
1472
1497
  try {
1473
1498
  const indexerUrl = opts.indexer || DEFAULT_INDEXER;
@@ -1649,6 +1674,154 @@ var balanceCmd = new import_commander.Command("balance").description("Check BNB
1649
1674
  process.exit(1);
1650
1675
  }
1651
1676
  });
1677
+ var historyCmd = new import_commander.Command("history").description("Show recent transactions executed through the agent vault").requiredOption("-k, --token-id <number>", "Agent NFA Token ID").option("--limit <number>", "Number of transactions to show", "10").option("--indexer <url>", "Indexer API URL", DEFAULT_INDEXER);
1678
+ historyCmd.action(async (opts) => {
1679
+ try {
1680
+ const tokenId = opts.tokenId;
1681
+ const limit = Number(opts.limit) || 10;
1682
+ const indexerUrl = opts.indexer || DEFAULT_INDEXER;
1683
+ const activityRes = await fetch(`${indexerUrl}/api/activity/${tokenId}?limit=${limit}`, {
1684
+ signal: AbortSignal.timeout(1e4)
1685
+ });
1686
+ if (!activityRes.ok) {
1687
+ output({ status: "error", message: `Indexer returned ${activityRes.status}. Is the indexer running?` });
1688
+ process.exit(1);
1689
+ }
1690
+ const data = await activityRes.json();
1691
+ let failures = [];
1692
+ try {
1693
+ const failRes = await fetch(`${indexerUrl}/api/agents/${tokenId}/commit-failures?limit=5`, {
1694
+ signal: AbortSignal.timeout(8e3)
1695
+ });
1696
+ if (failRes.ok) {
1697
+ const failData = await failRes.json();
1698
+ failures = failData.items || [];
1699
+ }
1700
+ } catch {
1701
+ }
1702
+ const transactions = (data.items || []).map((tx) => {
1703
+ const date = new Date(Number(tx.timestamp) * 1e3);
1704
+ return {
1705
+ time: date.toISOString(),
1706
+ txHash: tx.txHash,
1707
+ target: tx.target,
1708
+ success: tx.success,
1709
+ bscscanUrl: `https://bscscan.com/tx/${tx.txHash}`
1710
+ };
1711
+ });
1712
+ output({
1713
+ status: "success",
1714
+ tokenId,
1715
+ transactions,
1716
+ totalShown: transactions.length,
1717
+ recentPolicyRejections: failures.length,
1718
+ policyRejections: failures.length > 0 ? failures : void 0
1719
+ });
1720
+ } catch (error) {
1721
+ const message = error instanceof Error ? error.message : "Unknown error";
1722
+ output({ status: "error", message });
1723
+ process.exit(1);
1724
+ }
1725
+ });
1726
+ var statusCmd = new import_commander.Command("status").description("Show a security overview: vault balance, operator status, policies, and recent activity").requiredOption("-k, --token-id <number>", "Agent NFA Token ID").option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC).option("--nfa-address <address>", "AgentNFA contract address", DEFAULT_NFA).option("--guard-address <address>", "PolicyGuard contract address", DEFAULT_GUARD).option("--indexer <url>", "Indexer API URL", DEFAULT_INDEXER);
1727
+ statusCmd.action(async (opts) => {
1728
+ try {
1729
+ const tokenId = BigInt(opts.tokenId);
1730
+ const rpcUrl = opts.rpc || DEFAULT_RPC;
1731
+ const nfaAddr = toHex(opts.nfaAddress || DEFAULT_NFA);
1732
+ const publicClient = (0, import_viem2.createPublicClient)({ chain: import_chains2.bsc, transport: (0, import_viem2.http)(rpcUrl) });
1733
+ const indexerUrl = opts.indexer || DEFAULT_INDEXER;
1734
+ const vault = await publicClient.readContract({
1735
+ address: nfaAddr,
1736
+ abi: AGENT_NFA_ABI,
1737
+ functionName: "accountOf",
1738
+ args: [tokenId]
1739
+ });
1740
+ const bnbBalance = await publicClient.getBalance({ address: vault });
1741
+ const bnbHuman = (Number(bnbBalance) / 1e18).toFixed(6);
1742
+ let operatorInfo = { configured: false };
1743
+ const pk = process.env.RUNNER_PRIVATE_KEY;
1744
+ if (pk) {
1745
+ const account = (0, import_accounts2.privateKeyToAccount)(toHex(pk));
1746
+ const opBalance = await publicClient.getBalance({ address: account.address });
1747
+ const opBnb = (Number(opBalance) / 1e18).toFixed(6);
1748
+ operatorInfo = {
1749
+ configured: true,
1750
+ address: account.address,
1751
+ gasBnb: opBnb,
1752
+ gasOk: Number(opBalance) > 1e15
1753
+ };
1754
+ }
1755
+ const client = createPolicyClient(opts);
1756
+ const policies = await client.getPolicies(tokenId);
1757
+ const summaryParts = [];
1758
+ for (const p of policies) {
1759
+ if (p.policyTypeName === "spending_limit") {
1760
+ try {
1761
+ const limits = await publicClient.readContract({
1762
+ address: p.address,
1763
+ abi: SPENDING_LIMIT_ABI,
1764
+ functionName: "instanceLimits",
1765
+ args: [tokenId]
1766
+ });
1767
+ const [maxPerTx, maxPerDay] = limits;
1768
+ summaryParts.push(`Max ${(Number(maxPerTx) / 1e18).toFixed(4)} BNB/tx, ${(Number(maxPerDay) / 1e18).toFixed(4)} BNB/day`);
1769
+ } catch {
1770
+ }
1771
+ }
1772
+ if (p.policyTypeName === "cooldown") {
1773
+ try {
1774
+ const cd = await publicClient.readContract({
1775
+ address: p.address,
1776
+ abi: COOLDOWN_ABI,
1777
+ functionName: "cooldownSeconds",
1778
+ args: [tokenId]
1779
+ });
1780
+ summaryParts.push(`Cooldown ${Number(cd)}s`);
1781
+ } catch {
1782
+ }
1783
+ }
1784
+ if (p.policyTypeName === "receiver_guard") summaryParts.push("ReceiverGuard active");
1785
+ if (p.policyTypeName === "dex_whitelist") summaryParts.push("DEX whitelist active");
1786
+ if (p.policyTypeName === "token_whitelist") summaryParts.push("Token whitelist active");
1787
+ if (p.policyTypeName === "defi_guard") summaryParts.push("DeFiGuard active");
1788
+ }
1789
+ let activityStats = { available: false };
1790
+ try {
1791
+ const summaryRes = await fetch(`${indexerUrl}/api/agents/${opts.tokenId}/summary`, {
1792
+ signal: AbortSignal.timeout(8e3)
1793
+ });
1794
+ if (summaryRes.ok) {
1795
+ const summaryData = await summaryRes.json();
1796
+ activityStats = {
1797
+ available: true,
1798
+ totalExecutions: summaryData.totalExecutions,
1799
+ successRate: summaryData.totalExecutions > 0 ? `${(summaryData.successCount / summaryData.totalExecutions * 100).toFixed(1)}%` : "N/A",
1800
+ lastExecution: summaryData.lastExecution ? new Date(Number(summaryData.lastExecution) * 1e3).toISOString() : null
1801
+ };
1802
+ }
1803
+ } catch {
1804
+ }
1805
+ output({
1806
+ status: "success",
1807
+ tokenId: tokenId.toString(),
1808
+ vault: {
1809
+ address: vault,
1810
+ bnbBalance: bnbHuman
1811
+ },
1812
+ operator: operatorInfo,
1813
+ securitySummary: summaryParts.length > 0 ? summaryParts.join(" | ") : "No policies found",
1814
+ policyCount: policies.length,
1815
+ activity: activityStats,
1816
+ securityNote: "Operator wallet CANNOT withdraw vault funds or transfer Agent NFT.",
1817
+ dashboardUrl: `https://shll.run/dashboard?tokenId=${tokenId}`
1818
+ });
1819
+ } catch (error) {
1820
+ const message = error instanceof Error ? error.message : "Unknown error";
1821
+ output({ status: "error", message });
1822
+ process.exit(1);
1823
+ }
1824
+ });
1652
1825
  program.addCommand(swapCmd);
1653
1826
  program.addCommand(rawCmd);
1654
1827
  program.addCommand(tokensCmd);
@@ -1665,4 +1838,6 @@ program.addCommand(setupGuideCmd);
1665
1838
  program.addCommand(listingsCmd);
1666
1839
  program.addCommand(genWalletCmd);
1667
1840
  program.addCommand(balanceCmd);
1841
+ program.addCommand(historyCmd);
1842
+ program.addCommand(statusCmd);
1668
1843
  program.parse(process.argv);
package/dist/index.mjs CHANGED
@@ -1356,6 +1356,7 @@ policiesCmd.action(async (opts) => {
1356
1356
  const publicClient = createPublicClient2({ chain: bsc2, transport: http2(rpcUrl) });
1357
1357
  const policies = await client.getPolicies(tokenId);
1358
1358
  const enriched = [];
1359
+ const summaryParts = [];
1359
1360
  for (const p of policies) {
1360
1361
  const entry = {
1361
1362
  name: p.policyTypeName,
@@ -1371,13 +1372,16 @@ policiesCmd.action(async (opts) => {
1371
1372
  args: [tokenId]
1372
1373
  });
1373
1374
  const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
1375
+ const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
1376
+ const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
1374
1377
  entry.currentConfig = {
1375
1378
  maxPerTx: maxPerTx.toString(),
1376
- maxPerTxBnb: (Number(maxPerTx) / 1e18).toFixed(4),
1379
+ maxPerTxBnb: txBnb,
1377
1380
  maxPerDay: maxPerDay.toString(),
1378
- maxPerDayBnb: (Number(maxPerDay) / 1e18).toFixed(4),
1381
+ maxPerDayBnb: dayBnb,
1379
1382
  maxSlippageBps: maxSlippageBps.toString()
1380
1383
  };
1384
+ summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
1381
1385
  } catch {
1382
1386
  }
1383
1387
  }
@@ -1389,13 +1393,34 @@ policiesCmd.action(async (opts) => {
1389
1393
  functionName: "cooldownSeconds",
1390
1394
  args: [tokenId]
1391
1395
  });
1392
- entry.currentConfig = { cooldownSeconds: cd.toString() };
1396
+ const secs = Number(cd);
1397
+ entry.currentConfig = { cooldownSeconds: secs.toString() };
1398
+ summaryParts.push(`Cooldown ${secs}s between transactions`);
1393
1399
  } catch {
1394
1400
  }
1395
1401
  }
1402
+ if (p.policyTypeName === "receiver_guard") {
1403
+ summaryParts.push("Outbound transfers restricted (ReceiverGuard)");
1404
+ }
1405
+ if (p.policyTypeName === "dex_whitelist") {
1406
+ summaryParts.push("Only whitelisted DEXs allowed");
1407
+ }
1408
+ if (p.policyTypeName === "token_whitelist") {
1409
+ summaryParts.push("Only whitelisted tokens allowed");
1410
+ }
1411
+ if (p.policyTypeName === "defi_guard") {
1412
+ summaryParts.push("DeFi interactions validated by DeFiGuard");
1413
+ }
1396
1414
  enriched.push(entry);
1397
1415
  }
1398
- output({ status: "success", tokenId: tokenId.toString(), policies: enriched });
1416
+ const humanSummary = summaryParts.length > 0 ? summaryParts.join(" | ") : "No configurable policies found";
1417
+ output({
1418
+ status: "success",
1419
+ tokenId: tokenId.toString(),
1420
+ humanSummary,
1421
+ securityNote: "Operator wallet CANNOT withdraw vault funds or transfer Agent NFT \u2014 only owner can.",
1422
+ policies: enriched
1423
+ });
1399
1424
  } catch (error) {
1400
1425
  const message = error instanceof Error ? error.message : "Unknown error";
1401
1426
  output({ status: "error", message });
@@ -1477,7 +1502,7 @@ configCmd.action(async (opts) => {
1477
1502
  process.exit(1);
1478
1503
  }
1479
1504
  });
1480
- var DEFAULT_INDEXER = "https://indexer.shll.run";
1505
+ var DEFAULT_INDEXER = "https://indexer-mainnet.shll.run";
1481
1506
  var listingsCmd = new Command("listings").description("List all available agent templates for rent").option("--indexer <url>", "Indexer API URL", DEFAULT_INDEXER).action(async (opts) => {
1482
1507
  try {
1483
1508
  const indexerUrl = opts.indexer || DEFAULT_INDEXER;
@@ -1659,6 +1684,154 @@ var balanceCmd = new Command("balance").description("Check BNB balance of the ga
1659
1684
  process.exit(1);
1660
1685
  }
1661
1686
  });
1687
+ var historyCmd = new Command("history").description("Show recent transactions executed through the agent vault").requiredOption("-k, --token-id <number>", "Agent NFA Token ID").option("--limit <number>", "Number of transactions to show", "10").option("--indexer <url>", "Indexer API URL", DEFAULT_INDEXER);
1688
+ historyCmd.action(async (opts) => {
1689
+ try {
1690
+ const tokenId = opts.tokenId;
1691
+ const limit = Number(opts.limit) || 10;
1692
+ const indexerUrl = opts.indexer || DEFAULT_INDEXER;
1693
+ const activityRes = await fetch(`${indexerUrl}/api/activity/${tokenId}?limit=${limit}`, {
1694
+ signal: AbortSignal.timeout(1e4)
1695
+ });
1696
+ if (!activityRes.ok) {
1697
+ output({ status: "error", message: `Indexer returned ${activityRes.status}. Is the indexer running?` });
1698
+ process.exit(1);
1699
+ }
1700
+ const data = await activityRes.json();
1701
+ let failures = [];
1702
+ try {
1703
+ const failRes = await fetch(`${indexerUrl}/api/agents/${tokenId}/commit-failures?limit=5`, {
1704
+ signal: AbortSignal.timeout(8e3)
1705
+ });
1706
+ if (failRes.ok) {
1707
+ const failData = await failRes.json();
1708
+ failures = failData.items || [];
1709
+ }
1710
+ } catch {
1711
+ }
1712
+ const transactions = (data.items || []).map((tx) => {
1713
+ const date = new Date(Number(tx.timestamp) * 1e3);
1714
+ return {
1715
+ time: date.toISOString(),
1716
+ txHash: tx.txHash,
1717
+ target: tx.target,
1718
+ success: tx.success,
1719
+ bscscanUrl: `https://bscscan.com/tx/${tx.txHash}`
1720
+ };
1721
+ });
1722
+ output({
1723
+ status: "success",
1724
+ tokenId,
1725
+ transactions,
1726
+ totalShown: transactions.length,
1727
+ recentPolicyRejections: failures.length,
1728
+ policyRejections: failures.length > 0 ? failures : void 0
1729
+ });
1730
+ } catch (error) {
1731
+ const message = error instanceof Error ? error.message : "Unknown error";
1732
+ output({ status: "error", message });
1733
+ process.exit(1);
1734
+ }
1735
+ });
1736
+ var statusCmd = new Command("status").description("Show a security overview: vault balance, operator status, policies, and recent activity").requiredOption("-k, --token-id <number>", "Agent NFA Token ID").option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC).option("--nfa-address <address>", "AgentNFA contract address", DEFAULT_NFA).option("--guard-address <address>", "PolicyGuard contract address", DEFAULT_GUARD).option("--indexer <url>", "Indexer API URL", DEFAULT_INDEXER);
1737
+ statusCmd.action(async (opts) => {
1738
+ try {
1739
+ const tokenId = BigInt(opts.tokenId);
1740
+ const rpcUrl = opts.rpc || DEFAULT_RPC;
1741
+ const nfaAddr = toHex(opts.nfaAddress || DEFAULT_NFA);
1742
+ const publicClient = createPublicClient2({ chain: bsc2, transport: http2(rpcUrl) });
1743
+ const indexerUrl = opts.indexer || DEFAULT_INDEXER;
1744
+ const vault = await publicClient.readContract({
1745
+ address: nfaAddr,
1746
+ abi: AGENT_NFA_ABI,
1747
+ functionName: "accountOf",
1748
+ args: [tokenId]
1749
+ });
1750
+ const bnbBalance = await publicClient.getBalance({ address: vault });
1751
+ const bnbHuman = (Number(bnbBalance) / 1e18).toFixed(6);
1752
+ let operatorInfo = { configured: false };
1753
+ const pk = process.env.RUNNER_PRIVATE_KEY;
1754
+ if (pk) {
1755
+ const account = privateKeyToAccount2(toHex(pk));
1756
+ const opBalance = await publicClient.getBalance({ address: account.address });
1757
+ const opBnb = (Number(opBalance) / 1e18).toFixed(6);
1758
+ operatorInfo = {
1759
+ configured: true,
1760
+ address: account.address,
1761
+ gasBnb: opBnb,
1762
+ gasOk: Number(opBalance) > 1e15
1763
+ };
1764
+ }
1765
+ const client = createPolicyClient(opts);
1766
+ const policies = await client.getPolicies(tokenId);
1767
+ const summaryParts = [];
1768
+ for (const p of policies) {
1769
+ if (p.policyTypeName === "spending_limit") {
1770
+ try {
1771
+ const limits = await publicClient.readContract({
1772
+ address: p.address,
1773
+ abi: SPENDING_LIMIT_ABI,
1774
+ functionName: "instanceLimits",
1775
+ args: [tokenId]
1776
+ });
1777
+ const [maxPerTx, maxPerDay] = limits;
1778
+ summaryParts.push(`Max ${(Number(maxPerTx) / 1e18).toFixed(4)} BNB/tx, ${(Number(maxPerDay) / 1e18).toFixed(4)} BNB/day`);
1779
+ } catch {
1780
+ }
1781
+ }
1782
+ if (p.policyTypeName === "cooldown") {
1783
+ try {
1784
+ const cd = await publicClient.readContract({
1785
+ address: p.address,
1786
+ abi: COOLDOWN_ABI,
1787
+ functionName: "cooldownSeconds",
1788
+ args: [tokenId]
1789
+ });
1790
+ summaryParts.push(`Cooldown ${Number(cd)}s`);
1791
+ } catch {
1792
+ }
1793
+ }
1794
+ if (p.policyTypeName === "receiver_guard") summaryParts.push("ReceiverGuard active");
1795
+ if (p.policyTypeName === "dex_whitelist") summaryParts.push("DEX whitelist active");
1796
+ if (p.policyTypeName === "token_whitelist") summaryParts.push("Token whitelist active");
1797
+ if (p.policyTypeName === "defi_guard") summaryParts.push("DeFiGuard active");
1798
+ }
1799
+ let activityStats = { available: false };
1800
+ try {
1801
+ const summaryRes = await fetch(`${indexerUrl}/api/agents/${opts.tokenId}/summary`, {
1802
+ signal: AbortSignal.timeout(8e3)
1803
+ });
1804
+ if (summaryRes.ok) {
1805
+ const summaryData = await summaryRes.json();
1806
+ activityStats = {
1807
+ available: true,
1808
+ totalExecutions: summaryData.totalExecutions,
1809
+ successRate: summaryData.totalExecutions > 0 ? `${(summaryData.successCount / summaryData.totalExecutions * 100).toFixed(1)}%` : "N/A",
1810
+ lastExecution: summaryData.lastExecution ? new Date(Number(summaryData.lastExecution) * 1e3).toISOString() : null
1811
+ };
1812
+ }
1813
+ } catch {
1814
+ }
1815
+ output({
1816
+ status: "success",
1817
+ tokenId: tokenId.toString(),
1818
+ vault: {
1819
+ address: vault,
1820
+ bnbBalance: bnbHuman
1821
+ },
1822
+ operator: operatorInfo,
1823
+ securitySummary: summaryParts.length > 0 ? summaryParts.join(" | ") : "No policies found",
1824
+ policyCount: policies.length,
1825
+ activity: activityStats,
1826
+ securityNote: "Operator wallet CANNOT withdraw vault funds or transfer Agent NFT.",
1827
+ dashboardUrl: `https://shll.run/dashboard?tokenId=${tokenId}`
1828
+ });
1829
+ } catch (error) {
1830
+ const message = error instanceof Error ? error.message : "Unknown error";
1831
+ output({ status: "error", message });
1832
+ process.exit(1);
1833
+ }
1834
+ });
1662
1835
  program.addCommand(swapCmd);
1663
1836
  program.addCommand(rawCmd);
1664
1837
  program.addCommand(tokensCmd);
@@ -1675,4 +1848,6 @@ program.addCommand(setupGuideCmd);
1675
1848
  program.addCommand(listingsCmd);
1676
1849
  program.addCommand(genWalletCmd);
1677
1850
  program.addCommand(balanceCmd);
1851
+ program.addCommand(historyCmd);
1852
+ program.addCommand(statusCmd);
1678
1853
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shll-skills",
3
- "version": "2.0.11",
3
+ "version": "2.1.0",
4
4
  "description": "SHLL Agent Runtime Skill for OpenClaw",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -21,4 +21,4 @@
21
21
  "typescript": "^5.4.5",
22
22
  "@types/node": "^20.12.7"
23
23
  }
24
- }
24
+ }
package/src/index.ts CHANGED
@@ -1088,6 +1088,7 @@ policiesCmd.action(async (opts) => {
1088
1088
 
1089
1089
  // Enrich with current config for configurable policies
1090
1090
  const enriched = [];
1091
+ const summaryParts: string[] = [];
1091
1092
  for (const p of policies) {
1092
1093
  const entry: Record<string, unknown> = {
1093
1094
  name: p.policyTypeName,
@@ -1104,13 +1105,16 @@ policiesCmd.action(async (opts) => {
1104
1105
  args: [tokenId],
1105
1106
  });
1106
1107
  const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
1108
+ const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
1109
+ const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
1107
1110
  entry.currentConfig = {
1108
1111
  maxPerTx: maxPerTx.toString(),
1109
- maxPerTxBnb: (Number(maxPerTx) / 1e18).toFixed(4),
1112
+ maxPerTxBnb: txBnb,
1110
1113
  maxPerDay: maxPerDay.toString(),
1111
- maxPerDayBnb: (Number(maxPerDay) / 1e18).toFixed(4),
1114
+ maxPerDayBnb: dayBnb,
1112
1115
  maxSlippageBps: maxSlippageBps.toString(),
1113
1116
  };
1117
+ summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
1114
1118
  } catch { /* policy read failed */ }
1115
1119
  }
1116
1120
 
@@ -1122,14 +1126,39 @@ policiesCmd.action(async (opts) => {
1122
1126
  functionName: "cooldownSeconds",
1123
1127
  args: [tokenId],
1124
1128
  });
1125
- entry.currentConfig = { cooldownSeconds: (cd as bigint).toString() };
1129
+ const secs = Number(cd);
1130
+ entry.currentConfig = { cooldownSeconds: secs.toString() };
1131
+ summaryParts.push(`Cooldown ${secs}s between transactions`);
1126
1132
  } catch { /* policy read failed */ }
1127
1133
  }
1128
1134
 
1135
+ if (p.policyTypeName === "receiver_guard") {
1136
+ summaryParts.push("Outbound transfers restricted (ReceiverGuard)");
1137
+ }
1138
+ if (p.policyTypeName === "dex_whitelist") {
1139
+ summaryParts.push("Only whitelisted DEXs allowed");
1140
+ }
1141
+ if (p.policyTypeName === "token_whitelist") {
1142
+ summaryParts.push("Only whitelisted tokens allowed");
1143
+ }
1144
+ if (p.policyTypeName === "defi_guard") {
1145
+ summaryParts.push("DeFi interactions validated by DeFiGuard");
1146
+ }
1147
+
1129
1148
  enriched.push(entry);
1130
1149
  }
1131
1150
 
1132
- output({ status: "success", tokenId: tokenId.toString(), policies: enriched });
1151
+ const humanSummary = summaryParts.length > 0
1152
+ ? summaryParts.join(" | ")
1153
+ : "No configurable policies found";
1154
+
1155
+ output({
1156
+ status: "success",
1157
+ tokenId: tokenId.toString(),
1158
+ humanSummary,
1159
+ securityNote: "Operator wallet CANNOT withdraw vault funds or transfer Agent NFT — only owner can.",
1160
+ policies: enriched,
1161
+ });
1133
1162
 
1134
1163
  } catch (error: unknown) {
1135
1164
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -1242,7 +1271,7 @@ configCmd.action(async (opts) => {
1242
1271
  });
1243
1272
 
1244
1273
  // -- Subcommand: listings (query available agent templates) --------
1245
- const DEFAULT_INDEXER = "https://indexer.shll.run";
1274
+ const DEFAULT_INDEXER = "https://indexer-mainnet.shll.run";
1246
1275
 
1247
1276
  const listingsCmd = new Command("listings")
1248
1277
  .description("List all available agent templates for rent")
@@ -1478,6 +1507,208 @@ const balanceCmd = new Command("balance")
1478
1507
  }
1479
1508
  });
1480
1509
 
1510
+ // -- Subcommand: history (recent vault transactions) ------------------
1511
+ const historyCmd = new Command("history")
1512
+ .description("Show recent transactions executed through the agent vault")
1513
+ .requiredOption("-k, --token-id <number>", "Agent NFA Token ID")
1514
+ .option("--limit <number>", "Number of transactions to show", "10")
1515
+ .option("--indexer <url>", "Indexer API URL", DEFAULT_INDEXER);
1516
+
1517
+ historyCmd.action(async (opts) => {
1518
+ try {
1519
+ const tokenId = opts.tokenId;
1520
+ const limit = Number(opts.limit) || 10;
1521
+ const indexerUrl = opts.indexer || DEFAULT_INDEXER;
1522
+
1523
+ // Fetch execution activity from indexer
1524
+ const activityRes = await fetch(`${indexerUrl}/api/activity/${tokenId}?limit=${limit}`, {
1525
+ signal: AbortSignal.timeout(10000),
1526
+ });
1527
+
1528
+ if (!activityRes.ok) {
1529
+ output({ status: "error", message: `Indexer returned ${activityRes.status}. Is the indexer running?` });
1530
+ process.exit(1);
1531
+ }
1532
+
1533
+ const data = await activityRes.json() as {
1534
+ items: Array<{
1535
+ txHash: string;
1536
+ target: string;
1537
+ success: boolean;
1538
+ timestamp: string;
1539
+ blockNumber: string;
1540
+ action?: string;
1541
+ }>;
1542
+ count: number;
1543
+ };
1544
+
1545
+ // Also fetch commit failures (policy rejections)
1546
+ let failures: Array<{
1547
+ txHash: string;
1548
+ reason: string;
1549
+ timestamp: string;
1550
+ }> = [];
1551
+ try {
1552
+ const failRes = await fetch(`${indexerUrl}/api/agents/${tokenId}/commit-failures?limit=5`, {
1553
+ signal: AbortSignal.timeout(8000),
1554
+ });
1555
+ if (failRes.ok) {
1556
+ const failData = await failRes.json() as { items: typeof failures };
1557
+ failures = failData.items || [];
1558
+ }
1559
+ } catch { /* non-critical */ }
1560
+
1561
+ const transactions = (data.items || []).map((tx) => {
1562
+ const date = new Date(Number(tx.timestamp) * 1000);
1563
+ return {
1564
+ time: date.toISOString(),
1565
+ txHash: tx.txHash,
1566
+ target: tx.target,
1567
+ success: tx.success,
1568
+ bscscanUrl: `https://bscscan.com/tx/${tx.txHash}`,
1569
+ };
1570
+ });
1571
+
1572
+ output({
1573
+ status: "success",
1574
+ tokenId,
1575
+ transactions,
1576
+ totalShown: transactions.length,
1577
+ recentPolicyRejections: failures.length,
1578
+ policyRejections: failures.length > 0 ? failures : undefined,
1579
+ });
1580
+
1581
+ } catch (error: unknown) {
1582
+ const message = error instanceof Error ? error.message : "Unknown error";
1583
+ output({ status: "error", message });
1584
+ process.exit(1);
1585
+ }
1586
+ });
1587
+
1588
+ // -- Subcommand: status (one-shot security overview) ------------------
1589
+ const statusCmd = new Command("status")
1590
+ .description("Show a security overview: vault balance, operator status, policies, and recent activity")
1591
+ .requiredOption("-k, --token-id <number>", "Agent NFA Token ID")
1592
+ .option("-r, --rpc <url>", "BSC RPC URL", DEFAULT_RPC)
1593
+ .option("--nfa-address <address>", "AgentNFA contract address", DEFAULT_NFA)
1594
+ .option("--guard-address <address>", "PolicyGuard contract address", DEFAULT_GUARD)
1595
+ .option("--indexer <url>", "Indexer API URL", DEFAULT_INDEXER);
1596
+
1597
+ statusCmd.action(async (opts) => {
1598
+ try {
1599
+ const tokenId = BigInt(opts.tokenId);
1600
+ const rpcUrl = opts.rpc || DEFAULT_RPC;
1601
+ const nfaAddr = toHex(opts.nfaAddress || DEFAULT_NFA) as Address;
1602
+ const publicClient = createPublicClient({ chain: bsc, transport: http(rpcUrl) });
1603
+ const indexerUrl = opts.indexer || DEFAULT_INDEXER;
1604
+
1605
+ // 1. Vault address and BNB balance
1606
+ const vault = await publicClient.readContract({
1607
+ address: nfaAddr,
1608
+ abi: AGENT_NFA_ABI,
1609
+ functionName: "accountOf",
1610
+ args: [tokenId],
1611
+ }) as Address;
1612
+ const bnbBalance = await publicClient.getBalance({ address: vault });
1613
+ const bnbHuman = (Number(bnbBalance) / 1e18).toFixed(6);
1614
+
1615
+ // 2. Operator wallet info
1616
+ let operatorInfo: Record<string, unknown> = { configured: false };
1617
+ const pk = process.env.RUNNER_PRIVATE_KEY;
1618
+ if (pk) {
1619
+ const account = privateKeyToAccount(toHex(pk) as Hex);
1620
+ const opBalance = await publicClient.getBalance({ address: account.address });
1621
+ const opBnb = (Number(opBalance) / 1e18).toFixed(6);
1622
+ operatorInfo = {
1623
+ configured: true,
1624
+ address: account.address,
1625
+ gasBnb: opBnb,
1626
+ gasOk: Number(opBalance) > 1e15,
1627
+ };
1628
+ }
1629
+
1630
+ // 3. Policies summary
1631
+ const client = createPolicyClient(opts);
1632
+ const policies = await client.getPolicies(tokenId);
1633
+ const summaryParts: string[] = [];
1634
+ for (const p of policies) {
1635
+ if (p.policyTypeName === "spending_limit") {
1636
+ try {
1637
+ const limits = await publicClient.readContract({
1638
+ address: p.address,
1639
+ abi: SPENDING_LIMIT_ABI,
1640
+ functionName: "instanceLimits",
1641
+ args: [tokenId],
1642
+ });
1643
+ const [maxPerTx, maxPerDay] = limits;
1644
+ summaryParts.push(`Max ${(Number(maxPerTx) / 1e18).toFixed(4)} BNB/tx, ${(Number(maxPerDay) / 1e18).toFixed(4)} BNB/day`);
1645
+ } catch { /* skip */ }
1646
+ }
1647
+ if (p.policyTypeName === "cooldown") {
1648
+ try {
1649
+ const cd = await publicClient.readContract({
1650
+ address: p.address,
1651
+ abi: COOLDOWN_ABI,
1652
+ functionName: "cooldownSeconds",
1653
+ args: [tokenId],
1654
+ });
1655
+ summaryParts.push(`Cooldown ${Number(cd)}s`);
1656
+ } catch { /* skip */ }
1657
+ }
1658
+ if (p.policyTypeName === "receiver_guard") summaryParts.push("ReceiverGuard active");
1659
+ if (p.policyTypeName === "dex_whitelist") summaryParts.push("DEX whitelist active");
1660
+ if (p.policyTypeName === "token_whitelist") summaryParts.push("Token whitelist active");
1661
+ if (p.policyTypeName === "defi_guard") summaryParts.push("DeFiGuard active");
1662
+ }
1663
+
1664
+ // 4. Recent activity stats from indexer
1665
+ let activityStats: Record<string, unknown> = { available: false };
1666
+ try {
1667
+ const summaryRes = await fetch(`${indexerUrl}/api/agents/${opts.tokenId}/summary`, {
1668
+ signal: AbortSignal.timeout(8000),
1669
+ });
1670
+ if (summaryRes.ok) {
1671
+ const summaryData = await summaryRes.json() as {
1672
+ totalExecutions: number;
1673
+ successCount: number;
1674
+ failCount: number;
1675
+ lastExecution: string | null;
1676
+ };
1677
+ activityStats = {
1678
+ available: true,
1679
+ totalExecutions: summaryData.totalExecutions,
1680
+ successRate: summaryData.totalExecutions > 0
1681
+ ? `${((summaryData.successCount / summaryData.totalExecutions) * 100).toFixed(1)}%`
1682
+ : "N/A",
1683
+ lastExecution: summaryData.lastExecution
1684
+ ? new Date(Number(summaryData.lastExecution) * 1000).toISOString()
1685
+ : null,
1686
+ };
1687
+ }
1688
+ } catch { /* non-critical */ }
1689
+
1690
+ output({
1691
+ status: "success",
1692
+ tokenId: tokenId.toString(),
1693
+ vault: {
1694
+ address: vault,
1695
+ bnbBalance: bnbHuman,
1696
+ },
1697
+ operator: operatorInfo,
1698
+ securitySummary: summaryParts.length > 0 ? summaryParts.join(" | ") : "No policies found",
1699
+ policyCount: policies.length,
1700
+ activity: activityStats,
1701
+ securityNote: "Operator wallet CANNOT withdraw vault funds or transfer Agent NFT.",
1702
+ dashboardUrl: `https://shll.run/dashboard?tokenId=${tokenId}`,
1703
+ });
1704
+
1705
+ } catch (error: unknown) {
1706
+ const message = error instanceof Error ? error.message : "Unknown error";
1707
+ output({ status: "error", message });
1708
+ process.exit(1);
1709
+ }
1710
+ });
1711
+
1481
1712
  program.addCommand(swapCmd);
1482
1713
  program.addCommand(rawCmd);
1483
1714
  program.addCommand(tokensCmd);
@@ -1494,5 +1725,7 @@ program.addCommand(setupGuideCmd);
1494
1725
  program.addCommand(listingsCmd);
1495
1726
  program.addCommand(genWalletCmd);
1496
1727
  program.addCommand(balanceCmd);
1728
+ program.addCommand(historyCmd);
1729
+ program.addCommand(statusCmd);
1497
1730
  program.parse(process.argv);
1498
1731