@gopherhole/cli 0.1.13 → 0.1.15

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 +264 -2
  2. package/package.json +1 -1
  3. package/src/index.ts +307 -2
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 = [
@@ -1087,11 +1087,16 @@ ${chalk_1.default.bold('Examples:')}
1087
1087
  $ gopherhole discover search "email assistant"
1088
1088
  $ gopherhole discover search --category productivity
1089
1089
  $ gopherhole discover search --tag ai --sort popular
1090
+ $ gopherhole discover search --skill-tag memory --scope tenant
1090
1091
  `)
1091
1092
  .option('-c, --category <category>', 'Filter by category')
1092
1093
  .option('-t, --tag <tag>', 'Filter by tag')
1094
+ .option('--skill-tag <skillTag>', 'Filter by skill tag')
1095
+ .option('--content-mode <contentMode>', 'Filter by content mode (MIME type)')
1093
1096
  .option('-s, --sort <sort>', 'Sort by: rating, popular, recent', 'rating')
1094
- .option('-l, --limit <limit>', 'Number of results', '20')
1097
+ .option('-l, --limit <limit>', 'Number of results (max 50)', '10')
1098
+ .option('-o, --offset <offset>', 'Pagination offset')
1099
+ .option('--scope <scope>', 'Scope: tenant (same-tenant agents only)')
1095
1100
  .action(async (query, options) => {
1096
1101
  const spinner = (0, ora_1.default)('Searching agents...').start();
1097
1102
  try {
@@ -1102,10 +1107,18 @@ ${chalk_1.default.bold('Examples:')}
1102
1107
  params.set('category', options.category);
1103
1108
  if (options.tag)
1104
1109
  params.set('tag', options.tag);
1110
+ if (options.skillTag)
1111
+ params.set('skillTag', options.skillTag);
1112
+ if (options.contentMode)
1113
+ params.set('contentMode', options.contentMode);
1105
1114
  if (options.sort)
1106
1115
  params.set('sort', options.sort);
1107
1116
  if (options.limit)
1108
1117
  params.set('limit', options.limit);
1118
+ if (options.offset)
1119
+ params.set('offset', options.offset);
1120
+ if (options.scope)
1121
+ params.set('scope', options.scope);
1109
1122
  log('GET /discover/agents?' + params.toString());
1110
1123
  const sessionId = config.get('sessionId');
1111
1124
  const headers = {};
@@ -1723,6 +1736,255 @@ access
1723
1736
  process.exit(1);
1724
1737
  }
1725
1738
  });
1739
+ // ========== BUDGET COMMAND ==========
1740
+ const budget = program
1741
+ .command('budget')
1742
+ .description(`View and manage agent spending limits
1743
+
1744
+ ${chalk_1.default.bold('Examples:')}
1745
+ $ gopherhole budget my-agent # View budget
1746
+ $ gopherhole budget my-agent -d 10 -w 50 # Set daily=$10, weekly=$50
1747
+ $ gopherhole budget my-agent --clear # Remove all limits
1748
+ $ gopherhole budget --all # List all agents' budgets
1749
+ `);
1750
+ budget
1751
+ .argument('[agentId]', 'Agent ID (optional with --all)')
1752
+ .option('-a, --all', 'List all agents\' budgets')
1753
+ .option('-d, --daily <amount>', 'Set daily limit (dollars)')
1754
+ .option('-w, --weekly <amount>', 'Set weekly limit (dollars)')
1755
+ .option('-r, --per-request <amount>', 'Set per-request max (dollars)')
1756
+ .option('--clear', 'Remove all spending limits')
1757
+ .option('--json', 'Output as JSON')
1758
+ .action(async (agentId, options) => {
1759
+ const sessionId = config.get('sessionId');
1760
+ if (!sessionId) {
1761
+ console.log(chalk_1.default.yellow('Not logged in.'));
1762
+ console.log(chalk_1.default.gray('Run: gopherhole login'));
1763
+ process.exit(1);
1764
+ }
1765
+ // Handle --all flag
1766
+ if (options.all) {
1767
+ const spinner = (0, ora_1.default)('Fetching spending overview...').start();
1768
+ try {
1769
+ const res = await fetch(`${API_URL}/spending`, {
1770
+ headers: { 'X-Session-ID': sessionId },
1771
+ });
1772
+ if (!res.ok) {
1773
+ throw new Error('Failed to fetch spending overview');
1774
+ }
1775
+ const data = await res.json();
1776
+ spinner.stop();
1777
+ if (options.json) {
1778
+ console.log(JSON.stringify(data, null, 2));
1779
+ return;
1780
+ }
1781
+ console.log(chalk_1.default.bold('\nšŸ’° Spending Overview\n'));
1782
+ console.log(` Today: $${data.totalSpentToday.toFixed(2)} This week: $${data.totalSpentThisWeek.toFixed(2)}\n`);
1783
+ if (data.agents.length === 0) {
1784
+ console.log(chalk_1.default.gray(' No agents found.\n'));
1785
+ return;
1786
+ }
1787
+ // Header
1788
+ console.log(chalk_1.default.gray(' Agent Daily Weekly Status'));
1789
+ console.log(chalk_1.default.gray(' ─'.padEnd(70, '─')));
1790
+ for (const agent of data.agents) {
1791
+ const name = agent.name.padEnd(18).slice(0, 18);
1792
+ let daily = 'āˆž'.padEnd(12);
1793
+ if (agent.daily?.limit !== null && agent.daily?.limit !== undefined) {
1794
+ const bar = getProgressBar(agent.daily.percent ?? 0, 5);
1795
+ daily = `$${agent.daily.spent.toFixed(0)}/$${agent.daily.limit.toFixed(0)} ${bar}`.padEnd(12);
1796
+ }
1797
+ else if (agent.daily?.spent) {
1798
+ daily = `$${agent.daily.spent.toFixed(2)}`.padEnd(12);
1799
+ }
1800
+ let weekly = 'āˆž'.padEnd(12);
1801
+ if (agent.weekly?.limit !== null && agent.weekly?.limit !== undefined) {
1802
+ const bar = getProgressBar(agent.weekly.percent ?? 0, 5);
1803
+ weekly = `$${agent.weekly.spent.toFixed(0)}/$${agent.weekly.limit.toFixed(0)} ${bar}`.padEnd(12);
1804
+ }
1805
+ else if (agent.weekly?.spent) {
1806
+ weekly = `$${agent.weekly.spent.toFixed(2)}`.padEnd(12);
1807
+ }
1808
+ const statusEmoji = {
1809
+ ok: brand.green('āœ… OK'),
1810
+ warning: chalk_1.default.yellow('āš ļø Warning'),
1811
+ critical: chalk_1.default.red('šŸ”“ Critical'),
1812
+ blocked: chalk_1.default.red('🚫 Blocked'),
1813
+ unlimited: chalk_1.default.gray('āˆž'),
1814
+ }[agent.status] ?? agent.status;
1815
+ console.log(` ${name} ${daily} ${weekly} ${statusEmoji}`);
1816
+ }
1817
+ console.log('');
1818
+ }
1819
+ catch (err) {
1820
+ spinner.fail(chalk_1.default.red(err.message));
1821
+ process.exit(1);
1822
+ }
1823
+ return;
1824
+ }
1825
+ // Need agentId for single-agent operations
1826
+ if (!agentId) {
1827
+ console.log(chalk_1.default.yellow('Please provide an agent ID or use --all'));
1828
+ console.log(chalk_1.default.gray('Usage: gopherhole budget <agentId> [options]'));
1829
+ console.log(chalk_1.default.gray(' gopherhole budget --all'));
1830
+ process.exit(1);
1831
+ }
1832
+ // Handle --clear flag
1833
+ if (options.clear) {
1834
+ const spinner = (0, ora_1.default)('Removing spending limits...').start();
1835
+ try {
1836
+ const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
1837
+ method: 'DELETE',
1838
+ headers: { 'X-Session-ID': sessionId },
1839
+ });
1840
+ if (!res.ok) {
1841
+ throw new Error('Failed to remove spending limits');
1842
+ }
1843
+ spinner.succeed('Spending limits removed');
1844
+ console.log(chalk_1.default.gray(` Agent ${agentId} now has no spending limits.\n`));
1845
+ }
1846
+ catch (err) {
1847
+ spinner.fail(chalk_1.default.red(err.message));
1848
+ process.exit(1);
1849
+ }
1850
+ return;
1851
+ }
1852
+ // Handle setting limits
1853
+ if (options.daily !== undefined || options.weekly !== undefined || options.perRequest !== undefined) {
1854
+ const spinner = (0, ora_1.default)('Updating spending limits...').start();
1855
+ try {
1856
+ const body = {};
1857
+ if (options.daily !== undefined) {
1858
+ body.dailyLimit = options.daily === 'unlimited' ? null : parseFloat(options.daily);
1859
+ }
1860
+ if (options.weekly !== undefined) {
1861
+ body.weeklyLimit = options.weekly === 'unlimited' ? null : parseFloat(options.weekly);
1862
+ }
1863
+ if (options.perRequest !== undefined) {
1864
+ body.maxPerRequest = options.perRequest === 'unlimited' ? null : parseFloat(options.perRequest);
1865
+ }
1866
+ const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
1867
+ method: 'PUT',
1868
+ headers: {
1869
+ 'X-Session-ID': sessionId,
1870
+ 'Content-Type': 'application/json',
1871
+ },
1872
+ body: JSON.stringify(body),
1873
+ });
1874
+ if (!res.ok) {
1875
+ const err = await res.json();
1876
+ throw new Error(err.error || 'Failed to update limits');
1877
+ }
1878
+ spinner.succeed('Spending limits updated');
1879
+ // Show the updated limits
1880
+ const data = await res.json();
1881
+ console.log(chalk_1.default.gray('\n New limits:'));
1882
+ if (data.limits.daily) {
1883
+ console.log(` Daily: $${data.limits.daily.limit.toFixed(2)}`);
1884
+ }
1885
+ if (data.limits.weekly) {
1886
+ console.log(` Weekly: $${data.limits.weekly.limit.toFixed(2)}`);
1887
+ }
1888
+ if (data.limits.maxPerRequest) {
1889
+ console.log(` Per Request: $${data.limits.maxPerRequest.toFixed(2)}`);
1890
+ }
1891
+ console.log('');
1892
+ }
1893
+ catch (err) {
1894
+ spinner.fail(chalk_1.default.red(err.message));
1895
+ process.exit(1);
1896
+ }
1897
+ return;
1898
+ }
1899
+ // Default: show budget
1900
+ const spinner = (0, ora_1.default)('Fetching budget...').start();
1901
+ try {
1902
+ const res = await fetch(`${API_URL}/agents/${agentId}/budget`, {
1903
+ headers: { 'X-Session-ID': sessionId },
1904
+ });
1905
+ if (!res.ok) {
1906
+ if (res.status === 404) {
1907
+ throw new Error('Agent not found');
1908
+ }
1909
+ throw new Error('Failed to fetch budget');
1910
+ }
1911
+ const data = await res.json();
1912
+ spinner.stop();
1913
+ if (options.json) {
1914
+ console.log(JSON.stringify(data, null, 2));
1915
+ return;
1916
+ }
1917
+ console.log(chalk_1.default.bold(`\nšŸ’° Budget for ${agentId}\n`));
1918
+ if (!data.daily && !data.weekly && !data.maxPerRequest) {
1919
+ console.log(chalk_1.default.gray(' No spending limits configured.\n'));
1920
+ console.log(chalk_1.default.gray(' Set limits: gopherhole budget <agentId> -d 10 -w 50\n'));
1921
+ return;
1922
+ }
1923
+ if (data.daily) {
1924
+ const percent = Math.round((data.daily.spent / data.daily.limit) * 100);
1925
+ const bar = getProgressBar(percent, 10);
1926
+ const timeLeft = getTimeUntil(data.daily.resetsAt);
1927
+ const color = percent >= 80 ? chalk_1.default.red : percent >= 50 ? chalk_1.default.yellow : brand.green;
1928
+ console.log(` Daily: ${color(`$${data.daily.spent.toFixed(2)} / $${data.daily.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
1929
+ console.log(chalk_1.default.gray(` Resets in ${timeLeft}`));
1930
+ }
1931
+ if (data.weekly) {
1932
+ const percent = Math.round((data.weekly.spent / data.weekly.limit) * 100);
1933
+ const bar = getProgressBar(percent, 10);
1934
+ const timeLeft = getTimeUntil(data.weekly.resetsAt);
1935
+ const color = percent >= 80 ? chalk_1.default.red : percent >= 50 ? chalk_1.default.yellow : brand.green;
1936
+ console.log(` Weekly: ${color(`$${data.weekly.spent.toFixed(2)} / $${data.weekly.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
1937
+ console.log(chalk_1.default.gray(` Resets ${timeLeft}`));
1938
+ }
1939
+ if (data.maxPerRequest) {
1940
+ console.log(` Per Request: $${data.maxPerRequest.toFixed(2)} max`);
1941
+ }
1942
+ // Status
1943
+ let status = brand.green('āœ… OK');
1944
+ const dailyPercent = data.daily ? (data.daily.spent / data.daily.limit) * 100 : 0;
1945
+ const weeklyPercent = data.weekly ? (data.weekly.spent / data.weekly.limit) * 100 : 0;
1946
+ const maxPercent = Math.max(dailyPercent, weeklyPercent);
1947
+ if (maxPercent >= 100) {
1948
+ status = chalk_1.default.red('🚫 Blocked');
1949
+ }
1950
+ else if (maxPercent >= 80) {
1951
+ status = chalk_1.default.red('šŸ”“ Critical');
1952
+ }
1953
+ else if (maxPercent >= 50) {
1954
+ status = chalk_1.default.yellow('āš ļø Warning');
1955
+ }
1956
+ console.log(`\n Status: ${status}\n`);
1957
+ }
1958
+ catch (err) {
1959
+ spinner.fail(chalk_1.default.red(err.message));
1960
+ process.exit(1);
1961
+ }
1962
+ });
1963
+ // Helper: progress bar
1964
+ function getProgressBar(percent, width) {
1965
+ const filled = Math.round((percent / 100) * width);
1966
+ const empty = width - filled;
1967
+ const color = percent >= 80 ? chalk_1.default.red : percent >= 50 ? chalk_1.default.yellow : brand.green;
1968
+ return color('ā–ˆ'.repeat(filled)) + chalk_1.default.gray('ā–‘'.repeat(empty));
1969
+ }
1970
+ // Helper: time until
1971
+ function getTimeUntil(isoDate) {
1972
+ const target = new Date(isoDate);
1973
+ const now = new Date();
1974
+ const diffMs = target.getTime() - now.getTime();
1975
+ if (diffMs <= 0)
1976
+ return 'now';
1977
+ const hours = Math.floor(diffMs / (1000 * 60 * 60));
1978
+ const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
1979
+ if (hours > 24) {
1980
+ const days = Math.floor(hours / 24);
1981
+ return `${days}d ${hours % 24}h`;
1982
+ }
1983
+ if (hours > 0) {
1984
+ return `${hours}h ${minutes}m`;
1985
+ }
1986
+ return `${minutes}m`;
1987
+ }
1726
1988
  // ========== STATUS COMMAND ==========
1727
1989
  program
1728
1990
  .command('status')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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) {
@@ -1208,11 +1208,16 @@ ${chalk.bold('Examples:')}
1208
1208
  $ gopherhole discover search "email assistant"
1209
1209
  $ gopherhole discover search --category productivity
1210
1210
  $ gopherhole discover search --tag ai --sort popular
1211
+ $ gopherhole discover search --skill-tag memory --scope tenant
1211
1212
  `)
1212
1213
  .option('-c, --category <category>', 'Filter by category')
1213
1214
  .option('-t, --tag <tag>', 'Filter by tag')
1215
+ .option('--skill-tag <skillTag>', 'Filter by skill tag')
1216
+ .option('--content-mode <contentMode>', 'Filter by content mode (MIME type)')
1214
1217
  .option('-s, --sort <sort>', 'Sort by: rating, popular, recent', 'rating')
1215
- .option('-l, --limit <limit>', 'Number of results', '20')
1218
+ .option('-l, --limit <limit>', 'Number of results (max 50)', '10')
1219
+ .option('-o, --offset <offset>', 'Pagination offset')
1220
+ .option('--scope <scope>', 'Scope: tenant (same-tenant agents only)')
1216
1221
  .action(async (query, options) => {
1217
1222
  const spinner = ora('Searching agents...').start();
1218
1223
 
@@ -1221,8 +1226,12 @@ ${chalk.bold('Examples:')}
1221
1226
  if (query) params.set('q', query);
1222
1227
  if (options.category) params.set('category', options.category);
1223
1228
  if (options.tag) params.set('tag', options.tag);
1229
+ if (options.skillTag) params.set('skillTag', options.skillTag);
1230
+ if (options.contentMode) params.set('contentMode', options.contentMode);
1224
1231
  if (options.sort) params.set('sort', options.sort);
1225
1232
  if (options.limit) params.set('limit', options.limit);
1233
+ if (options.offset) params.set('offset', options.offset);
1234
+ if (options.scope) params.set('scope', options.scope);
1226
1235
 
1227
1236
  log('GET /discover/agents?' + params.toString());
1228
1237
  const sessionId = config.get('sessionId') as string | undefined;
@@ -1891,6 +1900,302 @@ access
1891
1900
  }
1892
1901
  });
1893
1902
 
1903
+ // ========== BUDGET COMMAND ==========
1904
+
1905
+ const budget = program
1906
+ .command('budget')
1907
+ .description(`View and manage agent spending limits
1908
+
1909
+ ${chalk.bold('Examples:')}
1910
+ $ gopherhole budget my-agent # View budget
1911
+ $ gopherhole budget my-agent -d 10 -w 50 # Set daily=$10, weekly=$50
1912
+ $ gopherhole budget my-agent --clear # Remove all limits
1913
+ $ gopherhole budget --all # List all agents' budgets
1914
+ `);
1915
+
1916
+ budget
1917
+ .argument('[agentId]', 'Agent ID (optional with --all)')
1918
+ .option('-a, --all', 'List all agents\' budgets')
1919
+ .option('-d, --daily <amount>', 'Set daily limit (dollars)')
1920
+ .option('-w, --weekly <amount>', 'Set weekly limit (dollars)')
1921
+ .option('-r, --per-request <amount>', 'Set per-request max (dollars)')
1922
+ .option('--clear', 'Remove all spending limits')
1923
+ .option('--json', 'Output as JSON')
1924
+ .action(async (agentId: string | undefined, options) => {
1925
+ const sessionId = config.get('sessionId') as string;
1926
+ if (!sessionId) {
1927
+ console.log(chalk.yellow('Not logged in.'));
1928
+ console.log(chalk.gray('Run: gopherhole login'));
1929
+ process.exit(1);
1930
+ }
1931
+
1932
+ // Handle --all flag
1933
+ if (options.all) {
1934
+ const spinner = ora('Fetching spending overview...').start();
1935
+ try {
1936
+ const res = await fetch(`${API_URL}/spending`, {
1937
+ headers: { 'X-Session-ID': sessionId },
1938
+ });
1939
+
1940
+ if (!res.ok) {
1941
+ throw new Error('Failed to fetch spending overview');
1942
+ }
1943
+
1944
+ const data = await res.json() as {
1945
+ agents: Array<{
1946
+ agentId: string;
1947
+ name: string;
1948
+ daily: { spent: number; limit: number | null; percent: number | null } | null;
1949
+ weekly: { spent: number; limit: number | null; percent: number | null } | null;
1950
+ status: string;
1951
+ }>;
1952
+ totalSpentToday: number;
1953
+ totalSpentThisWeek: number;
1954
+ };
1955
+ spinner.stop();
1956
+
1957
+ if (options.json) {
1958
+ console.log(JSON.stringify(data, null, 2));
1959
+ return;
1960
+ }
1961
+
1962
+ console.log(chalk.bold('\nšŸ’° Spending Overview\n'));
1963
+ console.log(` Today: $${data.totalSpentToday.toFixed(2)} This week: $${data.totalSpentThisWeek.toFixed(2)}\n`);
1964
+
1965
+ if (data.agents.length === 0) {
1966
+ console.log(chalk.gray(' No agents found.\n'));
1967
+ return;
1968
+ }
1969
+
1970
+ // Header
1971
+ console.log(chalk.gray(' Agent Daily Weekly Status'));
1972
+ console.log(chalk.gray(' ─'.padEnd(70, '─')));
1973
+
1974
+ for (const agent of data.agents) {
1975
+ const name = agent.name.padEnd(18).slice(0, 18);
1976
+
1977
+ let daily = 'āˆž'.padEnd(12);
1978
+ if (agent.daily?.limit !== null && agent.daily?.limit !== undefined) {
1979
+ const bar = getProgressBar(agent.daily.percent ?? 0, 5);
1980
+ daily = `$${agent.daily.spent.toFixed(0)}/$${agent.daily.limit.toFixed(0)} ${bar}`.padEnd(12);
1981
+ } else if (agent.daily?.spent) {
1982
+ daily = `$${agent.daily.spent.toFixed(2)}`.padEnd(12);
1983
+ }
1984
+
1985
+ let weekly = 'āˆž'.padEnd(12);
1986
+ if (agent.weekly?.limit !== null && agent.weekly?.limit !== undefined) {
1987
+ const bar = getProgressBar(agent.weekly.percent ?? 0, 5);
1988
+ weekly = `$${agent.weekly.spent.toFixed(0)}/$${agent.weekly.limit.toFixed(0)} ${bar}`.padEnd(12);
1989
+ } else if (agent.weekly?.spent) {
1990
+ weekly = `$${agent.weekly.spent.toFixed(2)}`.padEnd(12);
1991
+ }
1992
+
1993
+ const statusEmoji = {
1994
+ ok: brand.green('āœ… OK'),
1995
+ warning: chalk.yellow('āš ļø Warning'),
1996
+ critical: chalk.red('šŸ”“ Critical'),
1997
+ blocked: chalk.red('🚫 Blocked'),
1998
+ unlimited: chalk.gray('āˆž'),
1999
+ }[agent.status] ?? agent.status;
2000
+
2001
+ console.log(` ${name} ${daily} ${weekly} ${statusEmoji}`);
2002
+ }
2003
+ console.log('');
2004
+ } catch (err) {
2005
+ spinner.fail(chalk.red((err as Error).message));
2006
+ process.exit(1);
2007
+ }
2008
+ return;
2009
+ }
2010
+
2011
+ // Need agentId for single-agent operations
2012
+ if (!agentId) {
2013
+ console.log(chalk.yellow('Please provide an agent ID or use --all'));
2014
+ console.log(chalk.gray('Usage: gopherhole budget <agentId> [options]'));
2015
+ console.log(chalk.gray(' gopherhole budget --all'));
2016
+ process.exit(1);
2017
+ }
2018
+
2019
+ // Handle --clear flag
2020
+ if (options.clear) {
2021
+ const spinner = ora('Removing spending limits...').start();
2022
+ try {
2023
+ const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
2024
+ method: 'DELETE',
2025
+ headers: { 'X-Session-ID': sessionId },
2026
+ });
2027
+
2028
+ if (!res.ok) {
2029
+ throw new Error('Failed to remove spending limits');
2030
+ }
2031
+
2032
+ spinner.succeed('Spending limits removed');
2033
+ console.log(chalk.gray(` Agent ${agentId} now has no spending limits.\n`));
2034
+ } catch (err) {
2035
+ spinner.fail(chalk.red((err as Error).message));
2036
+ process.exit(1);
2037
+ }
2038
+ return;
2039
+ }
2040
+
2041
+ // Handle setting limits
2042
+ if (options.daily !== undefined || options.weekly !== undefined || options.perRequest !== undefined) {
2043
+ const spinner = ora('Updating spending limits...').start();
2044
+ try {
2045
+ const body: Record<string, number | null> = {};
2046
+
2047
+ if (options.daily !== undefined) {
2048
+ body.dailyLimit = options.daily === 'unlimited' ? null : parseFloat(options.daily);
2049
+ }
2050
+ if (options.weekly !== undefined) {
2051
+ body.weeklyLimit = options.weekly === 'unlimited' ? null : parseFloat(options.weekly);
2052
+ }
2053
+ if (options.perRequest !== undefined) {
2054
+ body.maxPerRequest = options.perRequest === 'unlimited' ? null : parseFloat(options.perRequest);
2055
+ }
2056
+
2057
+ const res = await fetch(`${API_URL}/agents/${agentId}/spending-limits`, {
2058
+ method: 'PUT',
2059
+ headers: {
2060
+ 'X-Session-ID': sessionId,
2061
+ 'Content-Type': 'application/json',
2062
+ },
2063
+ body: JSON.stringify(body),
2064
+ });
2065
+
2066
+ if (!res.ok) {
2067
+ const err = await res.json();
2068
+ throw new Error((err as { error?: string }).error || 'Failed to update limits');
2069
+ }
2070
+
2071
+ spinner.succeed('Spending limits updated');
2072
+
2073
+ // Show the updated limits
2074
+ const data = await res.json() as { limits: Record<string, unknown> };
2075
+ console.log(chalk.gray('\n New limits:'));
2076
+ if ((data.limits as any).daily) {
2077
+ console.log(` Daily: $${((data.limits as any).daily.limit as number).toFixed(2)}`);
2078
+ }
2079
+ if ((data.limits as any).weekly) {
2080
+ console.log(` Weekly: $${((data.limits as any).weekly.limit as number).toFixed(2)}`);
2081
+ }
2082
+ if ((data.limits as any).maxPerRequest) {
2083
+ console.log(` Per Request: $${((data.limits as any).maxPerRequest as number).toFixed(2)}`);
2084
+ }
2085
+ console.log('');
2086
+ } catch (err) {
2087
+ spinner.fail(chalk.red((err as Error).message));
2088
+ process.exit(1);
2089
+ }
2090
+ return;
2091
+ }
2092
+
2093
+ // Default: show budget
2094
+ const spinner = ora('Fetching budget...').start();
2095
+ try {
2096
+ const res = await fetch(`${API_URL}/agents/${agentId}/budget`, {
2097
+ headers: { 'X-Session-ID': sessionId },
2098
+ });
2099
+
2100
+ if (!res.ok) {
2101
+ if (res.status === 404) {
2102
+ throw new Error('Agent not found');
2103
+ }
2104
+ throw new Error('Failed to fetch budget');
2105
+ }
2106
+
2107
+ const data = await res.json() as {
2108
+ daily: { limit: number; spent: number; remaining: number; resetsAt: string } | null;
2109
+ weekly: { limit: number; spent: number; remaining: number; resetsAt: string } | null;
2110
+ maxPerRequest: number | null;
2111
+ };
2112
+ spinner.stop();
2113
+
2114
+ if (options.json) {
2115
+ console.log(JSON.stringify(data, null, 2));
2116
+ return;
2117
+ }
2118
+
2119
+ console.log(chalk.bold(`\nšŸ’° Budget for ${agentId}\n`));
2120
+
2121
+ if (!data.daily && !data.weekly && !data.maxPerRequest) {
2122
+ console.log(chalk.gray(' No spending limits configured.\n'));
2123
+ console.log(chalk.gray(' Set limits: gopherhole budget <agentId> -d 10 -w 50\n'));
2124
+ return;
2125
+ }
2126
+
2127
+ if (data.daily) {
2128
+ const percent = Math.round((data.daily.spent / data.daily.limit) * 100);
2129
+ const bar = getProgressBar(percent, 10);
2130
+ const timeLeft = getTimeUntil(data.daily.resetsAt);
2131
+ const color = percent >= 80 ? chalk.red : percent >= 50 ? chalk.yellow : brand.green;
2132
+ console.log(` Daily: ${color(`$${data.daily.spent.toFixed(2)} / $${data.daily.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
2133
+ console.log(chalk.gray(` Resets in ${timeLeft}`));
2134
+ }
2135
+
2136
+ if (data.weekly) {
2137
+ const percent = Math.round((data.weekly.spent / data.weekly.limit) * 100);
2138
+ const bar = getProgressBar(percent, 10);
2139
+ const timeLeft = getTimeUntil(data.weekly.resetsAt);
2140
+ const color = percent >= 80 ? chalk.red : percent >= 50 ? chalk.yellow : brand.green;
2141
+ console.log(` Weekly: ${color(`$${data.weekly.spent.toFixed(2)} / $${data.weekly.limit.toFixed(2)}`)} (${percent}%) ${bar}`);
2142
+ console.log(chalk.gray(` Resets ${timeLeft}`));
2143
+ }
2144
+
2145
+ if (data.maxPerRequest) {
2146
+ console.log(` Per Request: $${data.maxPerRequest.toFixed(2)} max`);
2147
+ }
2148
+
2149
+ // Status
2150
+ let status = brand.green('āœ… OK');
2151
+ const dailyPercent = data.daily ? (data.daily.spent / data.daily.limit) * 100 : 0;
2152
+ const weeklyPercent = data.weekly ? (data.weekly.spent / data.weekly.limit) * 100 : 0;
2153
+ const maxPercent = Math.max(dailyPercent, weeklyPercent);
2154
+
2155
+ if (maxPercent >= 100) {
2156
+ status = chalk.red('🚫 Blocked');
2157
+ } else if (maxPercent >= 80) {
2158
+ status = chalk.red('šŸ”“ Critical');
2159
+ } else if (maxPercent >= 50) {
2160
+ status = chalk.yellow('āš ļø Warning');
2161
+ }
2162
+
2163
+ console.log(`\n Status: ${status}\n`);
2164
+ } catch (err) {
2165
+ spinner.fail(chalk.red((err as Error).message));
2166
+ process.exit(1);
2167
+ }
2168
+ });
2169
+
2170
+ // Helper: progress bar
2171
+ function getProgressBar(percent: number, width: number): string {
2172
+ const filled = Math.round((percent / 100) * width);
2173
+ const empty = width - filled;
2174
+ const color = percent >= 80 ? chalk.red : percent >= 50 ? chalk.yellow : brand.green;
2175
+ return color('ā–ˆ'.repeat(filled)) + chalk.gray('ā–‘'.repeat(empty));
2176
+ }
2177
+
2178
+ // Helper: time until
2179
+ function getTimeUntil(isoDate: string): string {
2180
+ const target = new Date(isoDate);
2181
+ const now = new Date();
2182
+ const diffMs = target.getTime() - now.getTime();
2183
+
2184
+ if (diffMs <= 0) return 'now';
2185
+
2186
+ const hours = Math.floor(diffMs / (1000 * 60 * 60));
2187
+ const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
2188
+
2189
+ if (hours > 24) {
2190
+ const days = Math.floor(hours / 24);
2191
+ return `${days}d ${hours % 24}h`;
2192
+ }
2193
+ if (hours > 0) {
2194
+ return `${hours}h ${minutes}m`;
2195
+ }
2196
+ return `${minutes}m`;
2197
+ }
2198
+
1894
2199
  // ========== STATUS COMMAND ==========
1895
2200
 
1896
2201
  program