@gopherhole/cli 0.1.12 → 0.1.14

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.
Files changed (3) hide show
  1. package/dist/index.js +254 -3
  2. package/package.json +1 -1
  3. package/src/index.ts +301 -3
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ const brand = {
19
19
  greenDark: chalk_1.default.hex('#16a34a'), // gopher-600 - emphasis
20
20
  };
21
21
  // Version
22
- const VERSION = '0.1.8';
22
+ const VERSION = '0.1.14';
23
23
  // ASCII art banner
24
24
  function showBanner(context) {
25
25
  const gopher = [
@@ -1131,7 +1131,8 @@ ${chalk_1.default.bold('Examples:')}
1131
1131
  accessBadge = chalk_1.default.blue('👤YOURS');
1132
1132
  }
1133
1133
  else if (agent.accessStatus === 'approved' || agent.accessStatus === 'open') {
1134
- accessBadge = chalk_1.default.green('✓ACCESS');
1134
+ const discountText = agent.discountPercent ? ` ${agent.discountPercent}% OFF` : '';
1135
+ accessBadge = chalk_1.default.green(`✓ACCESS${discountText}`);
1135
1136
  }
1136
1137
  else if (agent.accessStatus === 'pending') {
1137
1138
  accessBadge = chalk_1.default.yellow('⏳PENDING');
@@ -1249,7 +1250,8 @@ ${chalk_1.default.bold('Example:')}
1249
1250
  accessLine = chalk_1.default.blue('👤 Your agent');
1250
1251
  }
1251
1252
  else if (agent.accessStatus === 'approved') {
1252
- accessLine = chalk_1.default.green('✓ You have access');
1253
+ const discountText = agent.discountPercent ? chalk_1.default.cyan(` (${agent.discountPercent}% discount)`) : '';
1254
+ accessLine = chalk_1.default.green('✓ You have access') + discountText;
1253
1255
  }
1254
1256
  else if (agent.accessStatus === 'open') {
1255
1257
  accessLine = chalk_1.default.green('✓ Open access (instant)');
@@ -1721,6 +1723,255 @@ access
1721
1723
  process.exit(1);
1722
1724
  }
1723
1725
  });
1726
+ // ========== BUDGET COMMAND ==========
1727
+ const budget = program
1728
+ .command('budget')
1729
+ .description(`View and manage agent spending limits
1730
+
1731
+ ${chalk_1.default.bold('Examples:')}
1732
+ $ gopherhole budget my-agent # View budget
1733
+ $ gopherhole budget my-agent -d 10 -w 50 # Set daily=$10, weekly=$50
1734
+ $ gopherhole budget my-agent --clear # Remove all limits
1735
+ $ gopherhole budget --all # List all agents' budgets
1736
+ `);
1737
+ budget
1738
+ .argument('[agentId]', 'Agent ID (optional with --all)')
1739
+ .option('-a, --all', 'List all agents\' budgets')
1740
+ .option('-d, --daily <amount>', 'Set daily limit (dollars)')
1741
+ .option('-w, --weekly <amount>', 'Set weekly limit (dollars)')
1742
+ .option('-r, --per-request <amount>', 'Set per-request max (dollars)')
1743
+ .option('--clear', 'Remove all spending limits')
1744
+ .option('--json', 'Output as JSON')
1745
+ .action(async (agentId, options) => {
1746
+ const sessionId = config.get('sessionId');
1747
+ if (!sessionId) {
1748
+ console.log(chalk_1.default.yellow('Not logged in.'));
1749
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
1750
+ process.exit(1);
1751
+ }
1752
+ // Handle --all flag
1753
+ if (options.all) {
1754
+ const spinner = (0, ora_1.default)('Fetching spending overview...').start();
1755
+ try {
1756
+ const res = await fetch(`${API_URL}/spending`, {
1757
+ headers: { 'X-Session-ID': sessionId },
1758
+ });
1759
+ if (!res.ok) {
1760
+ throw new Error('Failed to fetch spending overview');
1761
+ }
1762
+ const data = await res.json();
1763
+ spinner.stop();
1764
+ if (options.json) {
1765
+ console.log(JSON.stringify(data, null, 2));
1766
+ return;
1767
+ }
1768
+ console.log(chalk_1.default.bold('\n💰 Spending Overview\n'));
1769
+ console.log(` Today: $${data.totalSpentToday.toFixed(2)} This week: $${data.totalSpentThisWeek.toFixed(2)}\n`);
1770
+ if (data.agents.length === 0) {
1771
+ console.log(chalk_1.default.gray(' No agents found.\n'));
1772
+ return;
1773
+ }
1774
+ // Header
1775
+ console.log(chalk_1.default.gray(' Agent Daily Weekly Status'));
1776
+ console.log(chalk_1.default.gray(' ─'.padEnd(70, '─')));
1777
+ for (const agent of data.agents) {
1778
+ const name = agent.name.padEnd(18).slice(0, 18);
1779
+ let daily = '∞'.padEnd(12);
1780
+ if (agent.daily?.limit !== null && agent.daily?.limit !== undefined) {
1781
+ const bar = getProgressBar(agent.daily.percent ?? 0, 5);
1782
+ daily = `$${agent.daily.spent.toFixed(0)}/$${agent.daily.limit.toFixed(0)} ${bar}`.padEnd(12);
1783
+ }
1784
+ else if (agent.daily?.spent) {
1785
+ daily = `$${agent.daily.spent.toFixed(2)}`.padEnd(12);
1786
+ }
1787
+ let weekly = '∞'.padEnd(12);
1788
+ if (agent.weekly?.limit !== null && agent.weekly?.limit !== undefined) {
1789
+ const bar = getProgressBar(agent.weekly.percent ?? 0, 5);
1790
+ weekly = `$${agent.weekly.spent.toFixed(0)}/$${agent.weekly.limit.toFixed(0)} ${bar}`.padEnd(12);
1791
+ }
1792
+ else if (agent.weekly?.spent) {
1793
+ weekly = `$${agent.weekly.spent.toFixed(2)}`.padEnd(12);
1794
+ }
1795
+ const statusEmoji = {
1796
+ ok: brand.green('✅ OK'),
1797
+ warning: chalk_1.default.yellow('⚠️ Warning'),
1798
+ critical: chalk_1.default.red('🔴 Critical'),
1799
+ blocked: chalk_1.default.red('🚫 Blocked'),
1800
+ unlimited: chalk_1.default.gray('∞'),
1801
+ }[agent.status] ?? agent.status;
1802
+ console.log(` ${name} ${daily} ${weekly} ${statusEmoji}`);
1803
+ }
1804
+ console.log('');
1805
+ }
1806
+ catch (err) {
1807
+ spinner.fail(chalk_1.default.red(err.message));
1808
+ process.exit(1);
1809
+ }
1810
+ return;
1811
+ }
1812
+ // Need agentId for single-agent operations
1813
+ if (!agentId) {
1814
+ console.log(chalk_1.default.yellow('Please provide an agent ID or use --all'));
1815
+ console.log(chalk_1.default.gray('Usage: gopherhole budget <agentId> [options]'));
1816
+ console.log(chalk_1.default.gray(' gopherhole budget --all'));
1817
+ process.exit(1);
1818
+ }
1819
+ // Handle --clear flag
1820
+ if (options.clear) {
1821
+ const spinner = (0, ora_1.default)('Removing spending limits...').start();
1822
+ try {
1823
+ const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
1824
+ method: 'DELETE',
1825
+ headers: { 'X-Session-ID': sessionId },
1826
+ });
1827
+ if (!res.ok) {
1828
+ throw new Error('Failed to remove spending limits');
1829
+ }
1830
+ spinner.succeed('Spending limits removed');
1831
+ console.log(chalk_1.default.gray(` Agent ${agentId} now has no spending limits.\n`));
1832
+ }
1833
+ catch (err) {
1834
+ spinner.fail(chalk_1.default.red(err.message));
1835
+ process.exit(1);
1836
+ }
1837
+ return;
1838
+ }
1839
+ // Handle setting limits
1840
+ if (options.daily !== undefined || options.weekly !== undefined || options.perRequest !== undefined) {
1841
+ const spinner = (0, ora_1.default)('Updating spending limits...').start();
1842
+ try {
1843
+ const body = {};
1844
+ if (options.daily !== undefined) {
1845
+ body.dailyLimit = options.daily === 'unlimited' ? null : parseFloat(options.daily);
1846
+ }
1847
+ if (options.weekly !== undefined) {
1848
+ body.weeklyLimit = options.weekly === 'unlimited' ? null : parseFloat(options.weekly);
1849
+ }
1850
+ if (options.perRequest !== undefined) {
1851
+ body.maxPerRequest = options.perRequest === 'unlimited' ? null : parseFloat(options.perRequest);
1852
+ }
1853
+ const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
1854
+ method: 'PUT',
1855
+ headers: {
1856
+ 'X-Session-ID': sessionId,
1857
+ 'Content-Type': 'application/json',
1858
+ },
1859
+ body: JSON.stringify(body),
1860
+ });
1861
+ if (!res.ok) {
1862
+ const err = await res.json();
1863
+ throw new Error(err.error || 'Failed to update limits');
1864
+ }
1865
+ spinner.succeed('Spending limits updated');
1866
+ // Show the updated limits
1867
+ const data = await res.json();
1868
+ console.log(chalk_1.default.gray('\n New limits:'));
1869
+ if (data.limits.daily) {
1870
+ console.log(` Daily: $${data.limits.daily.limit.toFixed(2)}`);
1871
+ }
1872
+ if (data.limits.weekly) {
1873
+ console.log(` Weekly: $${data.limits.weekly.limit.toFixed(2)}`);
1874
+ }
1875
+ if (data.limits.maxPerRequest) {
1876
+ console.log(` Per Request: $${data.limits.maxPerRequest.toFixed(2)}`);
1877
+ }
1878
+ console.log('');
1879
+ }
1880
+ catch (err) {
1881
+ spinner.fail(chalk_1.default.red(err.message));
1882
+ process.exit(1);
1883
+ }
1884
+ return;
1885
+ }
1886
+ // Default: show budget
1887
+ const spinner = (0, ora_1.default)('Fetching budget...').start();
1888
+ try {
1889
+ const res = await fetch(`${API_URL}/agents/${agentId}/budget`, {
1890
+ headers: { 'X-Session-ID': sessionId },
1891
+ });
1892
+ if (!res.ok) {
1893
+ if (res.status === 404) {
1894
+ throw new Error('Agent not found');
1895
+ }
1896
+ throw new Error('Failed to fetch budget');
1897
+ }
1898
+ const data = await res.json();
1899
+ spinner.stop();
1900
+ if (options.json) {
1901
+ console.log(JSON.stringify(data, null, 2));
1902
+ return;
1903
+ }
1904
+ console.log(chalk_1.default.bold(`\n💰 Budget for ${agentId}\n`));
1905
+ if (!data.daily && !data.weekly && !data.maxPerRequest) {
1906
+ console.log(chalk_1.default.gray(' No spending limits configured.\n'));
1907
+ console.log(chalk_1.default.gray(' Set limits: gopherhole budget <agentId> -d 10 -w 50\n'));
1908
+ return;
1909
+ }
1910
+ if (data.daily) {
1911
+ const percent = Math.round((data.daily.spent / data.daily.limit) * 100);
1912
+ const bar = getProgressBar(percent, 10);
1913
+ const timeLeft = getTimeUntil(data.daily.resetsAt);
1914
+ const color = percent >= 80 ? chalk_1.default.red : percent >= 50 ? chalk_1.default.yellow : brand.green;
1915
+ console.log(` Daily: ${color(`$${data.daily.spent.toFixed(2)} / $${data.daily.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
1916
+ console.log(chalk_1.default.gray(` Resets in ${timeLeft}`));
1917
+ }
1918
+ if (data.weekly) {
1919
+ const percent = Math.round((data.weekly.spent / data.weekly.limit) * 100);
1920
+ const bar = getProgressBar(percent, 10);
1921
+ const timeLeft = getTimeUntil(data.weekly.resetsAt);
1922
+ const color = percent >= 80 ? chalk_1.default.red : percent >= 50 ? chalk_1.default.yellow : brand.green;
1923
+ console.log(` Weekly: ${color(`$${data.weekly.spent.toFixed(2)} / $${data.weekly.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
1924
+ console.log(chalk_1.default.gray(` Resets ${timeLeft}`));
1925
+ }
1926
+ if (data.maxPerRequest) {
1927
+ console.log(` Per Request: $${data.maxPerRequest.toFixed(2)} max`);
1928
+ }
1929
+ // Status
1930
+ let status = brand.green('✅ OK');
1931
+ const dailyPercent = data.daily ? (data.daily.spent / data.daily.limit) * 100 : 0;
1932
+ const weeklyPercent = data.weekly ? (data.weekly.spent / data.weekly.limit) * 100 : 0;
1933
+ const maxPercent = Math.max(dailyPercent, weeklyPercent);
1934
+ if (maxPercent >= 100) {
1935
+ status = chalk_1.default.red('🚫 Blocked');
1936
+ }
1937
+ else if (maxPercent >= 80) {
1938
+ status = chalk_1.default.red('🔴 Critical');
1939
+ }
1940
+ else if (maxPercent >= 50) {
1941
+ status = chalk_1.default.yellow('⚠️ Warning');
1942
+ }
1943
+ console.log(`\n Status: ${status}\n`);
1944
+ }
1945
+ catch (err) {
1946
+ spinner.fail(chalk_1.default.red(err.message));
1947
+ process.exit(1);
1948
+ }
1949
+ });
1950
+ // Helper: progress bar
1951
+ function getProgressBar(percent, width) {
1952
+ const filled = Math.round((percent / 100) * width);
1953
+ const empty = width - filled;
1954
+ const color = percent >= 80 ? chalk_1.default.red : percent >= 50 ? chalk_1.default.yellow : brand.green;
1955
+ return color('█'.repeat(filled)) + chalk_1.default.gray('░'.repeat(empty));
1956
+ }
1957
+ // Helper: time until
1958
+ function getTimeUntil(isoDate) {
1959
+ const target = new Date(isoDate);
1960
+ const now = new Date();
1961
+ const diffMs = target.getTime() - now.getTime();
1962
+ if (diffMs <= 0)
1963
+ return 'now';
1964
+ const hours = Math.floor(diffMs / (1000 * 60 * 60));
1965
+ const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
1966
+ if (hours > 24) {
1967
+ const days = Math.floor(hours / 24);
1968
+ return `${days}d ${hours % 24}h`;
1969
+ }
1970
+ if (hours > 0) {
1971
+ return `${hours}h ${minutes}m`;
1972
+ }
1973
+ return `${minutes}m`;
1974
+ }
1724
1975
  // ========== STATUS COMMAND ==========
1725
1976
  program
1726
1977
  .command('status')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "GopherHole CLI - Connect AI agents to the world",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -18,7 +18,7 @@ const brand = {
18
18
  };
19
19
 
20
20
  // Version
21
- const VERSION = '0.1.8';
21
+ const VERSION = '0.1.14';
22
22
 
23
23
  // ASCII art banner
24
24
  function showBanner(context?: string) {
@@ -1251,7 +1251,8 @@ ${chalk.bold('Examples:')}
1251
1251
  if (agent.accessStatus === 'owner') {
1252
1252
  accessBadge = chalk.blue('👤YOURS');
1253
1253
  } else if (agent.accessStatus === 'approved' || agent.accessStatus === 'open') {
1254
- accessBadge = chalk.green('✓ACCESS');
1254
+ const discountText = agent.discountPercent ? ` ${agent.discountPercent}% OFF` : '';
1255
+ accessBadge = chalk.green(`✓ACCESS${discountText}`);
1255
1256
  } else if (agent.accessStatus === 'pending') {
1256
1257
  accessBadge = chalk.yellow('⏳PENDING');
1257
1258
  } else if (agent.accessStatus === 'denied') {
@@ -1375,7 +1376,8 @@ ${chalk.bold('Example:')}
1375
1376
  if (agent.accessStatus === 'owner') {
1376
1377
  accessLine = chalk.blue('👤 Your agent');
1377
1378
  } else if (agent.accessStatus === 'approved') {
1378
- accessLine = chalk.green('✓ You have access');
1379
+ const discountText = agent.discountPercent ? chalk.cyan(` (${agent.discountPercent}% discount)`) : '';
1380
+ accessLine = chalk.green('✓ You have access') + discountText;
1379
1381
  } else if (agent.accessStatus === 'open') {
1380
1382
  accessLine = chalk.green('✓ Open access (instant)');
1381
1383
  } else if (agent.accessStatus === 'pending') {
@@ -1889,6 +1891,302 @@ access
1889
1891
  }
1890
1892
  });
1891
1893
 
1894
+ // ========== BUDGET COMMAND ==========
1895
+
1896
+ const budget = program
1897
+ .command('budget')
1898
+ .description(`View and manage agent spending limits
1899
+
1900
+ ${chalk.bold('Examples:')}
1901
+ $ gopherhole budget my-agent # View budget
1902
+ $ gopherhole budget my-agent -d 10 -w 50 # Set daily=$10, weekly=$50
1903
+ $ gopherhole budget my-agent --clear # Remove all limits
1904
+ $ gopherhole budget --all # List all agents' budgets
1905
+ `);
1906
+
1907
+ budget
1908
+ .argument('[agentId]', 'Agent ID (optional with --all)')
1909
+ .option('-a, --all', 'List all agents\' budgets')
1910
+ .option('-d, --daily <amount>', 'Set daily limit (dollars)')
1911
+ .option('-w, --weekly <amount>', 'Set weekly limit (dollars)')
1912
+ .option('-r, --per-request <amount>', 'Set per-request max (dollars)')
1913
+ .option('--clear', 'Remove all spending limits')
1914
+ .option('--json', 'Output as JSON')
1915
+ .action(async (agentId: string | undefined, options) => {
1916
+ const sessionId = config.get('sessionId') as string;
1917
+ if (!sessionId) {
1918
+ console.log(chalk.yellow('Not logged in.'));
1919
+ console.log(chalk.gray('Run: gopherhole login'));
1920
+ process.exit(1);
1921
+ }
1922
+
1923
+ // Handle --all flag
1924
+ if (options.all) {
1925
+ const spinner = ora('Fetching spending overview...').start();
1926
+ try {
1927
+ const res = await fetch(`${API_URL}/spending`, {
1928
+ headers: { 'X-Session-ID': sessionId },
1929
+ });
1930
+
1931
+ if (!res.ok) {
1932
+ throw new Error('Failed to fetch spending overview');
1933
+ }
1934
+
1935
+ const data = await res.json() as {
1936
+ agents: Array<{
1937
+ agentId: string;
1938
+ name: string;
1939
+ daily: { spent: number; limit: number | null; percent: number | null } | null;
1940
+ weekly: { spent: number; limit: number | null; percent: number | null } | null;
1941
+ status: string;
1942
+ }>;
1943
+ totalSpentToday: number;
1944
+ totalSpentThisWeek: number;
1945
+ };
1946
+ spinner.stop();
1947
+
1948
+ if (options.json) {
1949
+ console.log(JSON.stringify(data, null, 2));
1950
+ return;
1951
+ }
1952
+
1953
+ console.log(chalk.bold('\n💰 Spending Overview\n'));
1954
+ console.log(` Today: $${data.totalSpentToday.toFixed(2)} This week: $${data.totalSpentThisWeek.toFixed(2)}\n`);
1955
+
1956
+ if (data.agents.length === 0) {
1957
+ console.log(chalk.gray(' No agents found.\n'));
1958
+ return;
1959
+ }
1960
+
1961
+ // Header
1962
+ console.log(chalk.gray(' Agent Daily Weekly Status'));
1963
+ console.log(chalk.gray(' ─'.padEnd(70, '─')));
1964
+
1965
+ for (const agent of data.agents) {
1966
+ const name = agent.name.padEnd(18).slice(0, 18);
1967
+
1968
+ let daily = '∞'.padEnd(12);
1969
+ if (agent.daily?.limit !== null && agent.daily?.limit !== undefined) {
1970
+ const bar = getProgressBar(agent.daily.percent ?? 0, 5);
1971
+ daily = `$${agent.daily.spent.toFixed(0)}/$${agent.daily.limit.toFixed(0)} ${bar}`.padEnd(12);
1972
+ } else if (agent.daily?.spent) {
1973
+ daily = `$${agent.daily.spent.toFixed(2)}`.padEnd(12);
1974
+ }
1975
+
1976
+ let weekly = '∞'.padEnd(12);
1977
+ if (agent.weekly?.limit !== null && agent.weekly?.limit !== undefined) {
1978
+ const bar = getProgressBar(agent.weekly.percent ?? 0, 5);
1979
+ weekly = `$${agent.weekly.spent.toFixed(0)}/$${agent.weekly.limit.toFixed(0)} ${bar}`.padEnd(12);
1980
+ } else if (agent.weekly?.spent) {
1981
+ weekly = `$${agent.weekly.spent.toFixed(2)}`.padEnd(12);
1982
+ }
1983
+
1984
+ const statusEmoji = {
1985
+ ok: brand.green('✅ OK'),
1986
+ warning: chalk.yellow('⚠️ Warning'),
1987
+ critical: chalk.red('🔴 Critical'),
1988
+ blocked: chalk.red('🚫 Blocked'),
1989
+ unlimited: chalk.gray('∞'),
1990
+ }[agent.status] ?? agent.status;
1991
+
1992
+ console.log(` ${name} ${daily} ${weekly} ${statusEmoji}`);
1993
+ }
1994
+ console.log('');
1995
+ } catch (err) {
1996
+ spinner.fail(chalk.red((err as Error).message));
1997
+ process.exit(1);
1998
+ }
1999
+ return;
2000
+ }
2001
+
2002
+ // Need agentId for single-agent operations
2003
+ if (!agentId) {
2004
+ console.log(chalk.yellow('Please provide an agent ID or use --all'));
2005
+ console.log(chalk.gray('Usage: gopherhole budget <agentId> [options]'));
2006
+ console.log(chalk.gray(' gopherhole budget --all'));
2007
+ process.exit(1);
2008
+ }
2009
+
2010
+ // Handle --clear flag
2011
+ if (options.clear) {
2012
+ const spinner = ora('Removing spending limits...').start();
2013
+ try {
2014
+ const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
2015
+ method: 'DELETE',
2016
+ headers: { 'X-Session-ID': sessionId },
2017
+ });
2018
+
2019
+ if (!res.ok) {
2020
+ throw new Error('Failed to remove spending limits');
2021
+ }
2022
+
2023
+ spinner.succeed('Spending limits removed');
2024
+ console.log(chalk.gray(` Agent ${agentId} now has no spending limits.\n`));
2025
+ } catch (err) {
2026
+ spinner.fail(chalk.red((err as Error).message));
2027
+ process.exit(1);
2028
+ }
2029
+ return;
2030
+ }
2031
+
2032
+ // Handle setting limits
2033
+ if (options.daily !== undefined || options.weekly !== undefined || options.perRequest !== undefined) {
2034
+ const spinner = ora('Updating spending limits...').start();
2035
+ try {
2036
+ const body: Record<string, number | null> = {};
2037
+
2038
+ if (options.daily !== undefined) {
2039
+ body.dailyLimit = options.daily === 'unlimited' ? null : parseFloat(options.daily);
2040
+ }
2041
+ if (options.weekly !== undefined) {
2042
+ body.weeklyLimit = options.weekly === 'unlimited' ? null : parseFloat(options.weekly);
2043
+ }
2044
+ if (options.perRequest !== undefined) {
2045
+ body.maxPerRequest = options.perRequest === 'unlimited' ? null : parseFloat(options.perRequest);
2046
+ }
2047
+
2048
+ const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
2049
+ method: 'PUT',
2050
+ headers: {
2051
+ 'X-Session-ID': sessionId,
2052
+ 'Content-Type': 'application/json',
2053
+ },
2054
+ body: JSON.stringify(body),
2055
+ });
2056
+
2057
+ if (!res.ok) {
2058
+ const err = await res.json();
2059
+ throw new Error((err as { error?: string }).error || 'Failed to update limits');
2060
+ }
2061
+
2062
+ spinner.succeed('Spending limits updated');
2063
+
2064
+ // Show the updated limits
2065
+ const data = await res.json() as { limits: Record<string, unknown> };
2066
+ console.log(chalk.gray('\n New limits:'));
2067
+ if ((data.limits as any).daily) {
2068
+ console.log(` Daily: $${((data.limits as any).daily.limit as number).toFixed(2)}`);
2069
+ }
2070
+ if ((data.limits as any).weekly) {
2071
+ console.log(` Weekly: $${((data.limits as any).weekly.limit as number).toFixed(2)}`);
2072
+ }
2073
+ if ((data.limits as any).maxPerRequest) {
2074
+ console.log(` Per Request: $${((data.limits as any).maxPerRequest as number).toFixed(2)}`);
2075
+ }
2076
+ console.log('');
2077
+ } catch (err) {
2078
+ spinner.fail(chalk.red((err as Error).message));
2079
+ process.exit(1);
2080
+ }
2081
+ return;
2082
+ }
2083
+
2084
+ // Default: show budget
2085
+ const spinner = ora('Fetching budget...').start();
2086
+ try {
2087
+ const res = await fetch(`${API_URL}/agents/${agentId}/budget`, {
2088
+ headers: { 'X-Session-ID': sessionId },
2089
+ });
2090
+
2091
+ if (!res.ok) {
2092
+ if (res.status === 404) {
2093
+ throw new Error('Agent not found');
2094
+ }
2095
+ throw new Error('Failed to fetch budget');
2096
+ }
2097
+
2098
+ const data = await res.json() as {
2099
+ daily: { limit: number; spent: number; remaining: number; resetsAt: string } | null;
2100
+ weekly: { limit: number; spent: number; remaining: number; resetsAt: string } | null;
2101
+ maxPerRequest: number | null;
2102
+ };
2103
+ spinner.stop();
2104
+
2105
+ if (options.json) {
2106
+ console.log(JSON.stringify(data, null, 2));
2107
+ return;
2108
+ }
2109
+
2110
+ console.log(chalk.bold(`\n💰 Budget for ${agentId}\n`));
2111
+
2112
+ if (!data.daily && !data.weekly && !data.maxPerRequest) {
2113
+ console.log(chalk.gray(' No spending limits configured.\n'));
2114
+ console.log(chalk.gray(' Set limits: gopherhole budget <agentId> -d 10 -w 50\n'));
2115
+ return;
2116
+ }
2117
+
2118
+ if (data.daily) {
2119
+ const percent = Math.round((data.daily.spent / data.daily.limit) * 100);
2120
+ const bar = getProgressBar(percent, 10);
2121
+ const timeLeft = getTimeUntil(data.daily.resetsAt);
2122
+ const color = percent >= 80 ? chalk.red : percent >= 50 ? chalk.yellow : brand.green;
2123
+ console.log(` Daily: ${color(`$${data.daily.spent.toFixed(2)} / $${data.daily.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
2124
+ console.log(chalk.gray(` Resets in ${timeLeft}`));
2125
+ }
2126
+
2127
+ if (data.weekly) {
2128
+ const percent = Math.round((data.weekly.spent / data.weekly.limit) * 100);
2129
+ const bar = getProgressBar(percent, 10);
2130
+ const timeLeft = getTimeUntil(data.weekly.resetsAt);
2131
+ const color = percent >= 80 ? chalk.red : percent >= 50 ? chalk.yellow : brand.green;
2132
+ console.log(` Weekly: ${color(`$${data.weekly.spent.toFixed(2)} / $${data.weekly.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
2133
+ console.log(chalk.gray(` Resets ${timeLeft}`));
2134
+ }
2135
+
2136
+ if (data.maxPerRequest) {
2137
+ console.log(` Per Request: $${data.maxPerRequest.toFixed(2)} max`);
2138
+ }
2139
+
2140
+ // Status
2141
+ let status = brand.green('✅ OK');
2142
+ const dailyPercent = data.daily ? (data.daily.spent / data.daily.limit) * 100 : 0;
2143
+ const weeklyPercent = data.weekly ? (data.weekly.spent / data.weekly.limit) * 100 : 0;
2144
+ const maxPercent = Math.max(dailyPercent, weeklyPercent);
2145
+
2146
+ if (maxPercent >= 100) {
2147
+ status = chalk.red('🚫 Blocked');
2148
+ } else if (maxPercent >= 80) {
2149
+ status = chalk.red('🔴 Critical');
2150
+ } else if (maxPercent >= 50) {
2151
+ status = chalk.yellow('⚠️ Warning');
2152
+ }
2153
+
2154
+ console.log(`\n Status: ${status}\n`);
2155
+ } catch (err) {
2156
+ spinner.fail(chalk.red((err as Error).message));
2157
+ process.exit(1);
2158
+ }
2159
+ });
2160
+
2161
+ // Helper: progress bar
2162
+ function getProgressBar(percent: number, width: number): string {
2163
+ const filled = Math.round((percent / 100) * width);
2164
+ const empty = width - filled;
2165
+ const color = percent >= 80 ? chalk.red : percent >= 50 ? chalk.yellow : brand.green;
2166
+ return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
2167
+ }
2168
+
2169
+ // Helper: time until
2170
+ function getTimeUntil(isoDate: string): string {
2171
+ const target = new Date(isoDate);
2172
+ const now = new Date();
2173
+ const diffMs = target.getTime() - now.getTime();
2174
+
2175
+ if (diffMs <= 0) return 'now';
2176
+
2177
+ const hours = Math.floor(diffMs / (1000 * 60 * 60));
2178
+ const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
2179
+
2180
+ if (hours > 24) {
2181
+ const days = Math.floor(hours / 24);
2182
+ return `${days}d ${hours % 24}h`;
2183
+ }
2184
+ if (hours > 0) {
2185
+ return `${hours}h ${minutes}m`;
2186
+ }
2187
+ return `${minutes}m`;
2188
+ }
2189
+
1892
2190
  // ========== STATUS COMMAND ==========
1893
2191
 
1894
2192
  program